@void/solid 0.10.13 → 0.20.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present, Cloudflare, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -115,7 +115,8 @@ request.
115
115
 
116
116
  ## Tests
117
117
 
118
- - `test/unit/use-form.test.ts`
119
118
  - `test/integration/solid-pages-playground.test.ts`
120
119
 
121
120
  The Solid integration test covers both SSR output and browser-side navigation/forms.
121
+
122
+ The [shared client](../../test/pages-client-contract.ts) and [island](../../test/pages-island-contract.ts) contracts run against this adapter in both dev and preview. Shared form equality is tested directly in [Void](../void/test/unit/form-value-equality.test.ts); tests do not copy the hook implementation.
package/dist/plugin.d.mts CHANGED
@@ -1,16 +1,14 @@
1
1
  import solid from "vite-plugin-solid";
2
2
  import { Plugin } from "vite";
3
-
4
3
  //#region src/plugin.d.ts
5
4
  type PrefetchConfig = {
6
5
  hoverDelay?: number;
7
6
  cacheFor?: number | string | [string, string];
8
7
  };
9
- type VoidSolidOptions = Readonly<{
8
+ export type VoidSolidOptions = Readonly<{
10
9
  solid?: Parameters<typeof solid>[0];
11
10
  viewTransitions?: boolean;
12
11
  prefetch?: PrefetchConfig;
13
12
  }>;
14
- declare function voidSolid(options?: VoidSolidOptions): Array<Plugin>;
15
- //#endregion
16
- export { VoidSolidOptions, voidSolid };
13
+ export declare function voidSolid(options?: VoidSolidOptions): Array<Plugin>;
14
+ //#endregion
package/dist/plugin.mjs CHANGED
@@ -203,7 +203,7 @@ function islandsPlugin(pagesDir, options) {
203
203
  });
204
204
  }
205
205
  const islandId = absPath ? normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "") : spec;
206
- const wrapperUrl = "/@void-island-wrap/island_" + wrapperId++ + ".tsx";
206
+ const wrapperUrl = WRAPPER_PREFIX + "island_" + wrapperId++ + ".tsx";
207
207
  wrapperMeta.set(wrapperUrl, {
208
208
  src: absPath || spec,
209
209
  islandId,
@@ -341,6 +341,7 @@ initIslands(islands, mountIsland);
341
341
  }
342
342
  //#endregion
343
343
  //#region src/plugin-pages.ts
344
+ const RENDERER_ID = "virtual:void-pages-renderer";
344
345
  const CLIENT_ENTRY_ID = "virtual:void-pages-client";
345
346
  /** Check if the pages directory has any non-island page files (.tsx/.jsx without .island.). */
346
347
  function hasNonIslandPages(dir) {
@@ -374,7 +375,7 @@ function pagesPlugin(pagesDir, options) {
374
375
  return pendingRescan;
375
376
  };
376
377
  const invalidatePageModules = (server) => {
377
- for (const env of Object.values(server.environments)) for (const id of ["\0virtual:void-pages-renderer", "\0virtual:void-pages-client"]) {
378
+ for (const env of Object.values(server.environments)) for (const id of ["\0" + RENDERER_ID, "\0" + CLIENT_ENTRY_ID]) {
378
379
  const mod = env.moduleGraph.getModuleById(id);
379
380
  if (mod) env.moduleGraph.invalidateModule(mod);
380
381
  }
@@ -395,11 +396,11 @@ function pagesPlugin(pagesDir, options) {
395
396
  async handler(id) {
396
397
  if (isDev && pendingRescan) await pendingRescan;
397
398
  if (!pageScan && existsSync(pagesDir)) await runRescan();
398
- if (id === "\0virtual:void-pages-renderer") {
399
+ if (id === "\0" + RENDERER_ID) {
399
400
  if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
400
401
  return generateRendererModule(pageScan, pagesDir);
401
402
  }
402
- if (id === "\0virtual:void-pages-client") {
403
+ if (id === "\0" + CLIENT_ENTRY_ID) {
403
404
  if (!pageScan) return "";
404
405
  return generateClientEntry(pageScan, pagesDir, options?.viewTransitions, options?.prefetch, resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }));
405
406
  }
@@ -0,0 +1,9 @@
1
+ import { Context } from "solid-js";
2
+ import { NavigationState, VoidRouter } from "void/pages-client";
3
+ //#region src/runtime/context.d.ts
4
+ export declare const SharedContext: Context<Record<string, unknown> | null>;
5
+ export declare const ErrorsContext: Context<Record<string, string> | (() => Record<string, string>) | null>;
6
+ export declare const ParamsContext: Context<Record<string, string> | null>;
7
+ export declare const RouterContext: Context<VoidRouter | null>;
8
+ export declare const NavigationContext: Context<(() => NavigationState) | null>;
9
+ //#endregion
@@ -0,0 +1,9 @@
1
+ import { createContext } from "solid-js";
2
+ //#region src/runtime/context.ts
3
+ const SharedContext = createContext(null);
4
+ const ErrorsContext = createContext(null);
5
+ const ParamsContext = createContext(null);
6
+ const RouterContext = createContext(null);
7
+ const NavigationContext = createContext(null);
8
+ //#endregion
9
+ export { ErrorsContext, NavigationContext, ParamsContext, RouterContext, SharedContext };
@@ -0,0 +1,103 @@
1
+ import { ErrorsContext, NavigationContext, ParamsContext, RouterContext, SharedContext } from "./context.mjs";
2
+ import { JSX } from "solid-js";
3
+ import { ActionResult, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
4
+ import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
5
+ import { ActionFormOptions, ActionUrl, ResolveActionBody, ResolveActionParams } from "void/routes";
6
+ //#region src/runtime/link.d.ts
7
+ type PrefetchStrategy = "hover" | "click" | "mount" | "visible";
8
+ type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
9
+ type NavigateEvent = {
10
+ preventDefault(): void;
11
+ };
12
+ type CommonProps = {
13
+ href: string;
14
+ method?: LinkMethod | Lowercase<LinkMethod>;
15
+ data?: Record<string, unknown>;
16
+ preserveScroll?: boolean;
17
+ preserveState?: boolean;
18
+ replace?: boolean;
19
+ reloadDocument?: boolean;
20
+ viewTransition?: boolean;
21
+ prefetch?: boolean | PrefetchStrategy | ReadonlyArray<PrefetchStrategy>;
22
+ cacheFor?: number | string | [string, string];
23
+ onNavigate?: (event: NavigateEvent) => void;
24
+ children?: JSX.Element;
25
+ };
26
+ type LinkProps = CommonProps & Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, keyof CommonProps> & Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, keyof CommonProps>;
27
+ export declare function Link(props: LinkProps): JSX.Element;
28
+ //#endregion
29
+ //#region src/runtime/use-router.d.ts
30
+ export declare function useRouter(): VoidRouter;
31
+ //#endregion
32
+ //#region src/runtime/use-params.d.ts
33
+ export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
34
+ //#endregion
35
+ //#region src/runtime/use-shared.d.ts
36
+ export declare function useShared<T = CloudContextVariables["shared"]>(): T;
37
+ //#endregion
38
+ //#region src/runtime/use-form.d.ts
39
+ type FormSubmitOptions = Omit<VisitOptions, "method" | "data"> & {
40
+ data?: Record<string, unknown>;
41
+ };
42
+ export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
43
+ data: ResolveActionBody<U>;
44
+ setData: <K extends keyof ResolveActionBody<U> & string>(field: K, value: ResolveActionBody<U>[K]) => void;
45
+ errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
46
+ error: VoidActionError | null;
47
+ pending: boolean;
48
+ hasChanges: boolean;
49
+ wasSuccessful: boolean;
50
+ recentlySuccessful: boolean;
51
+ post(opts?: FormSubmitOptions): Promise<void>;
52
+ put(opts?: FormSubmitOptions): Promise<void>;
53
+ patch(opts?: FormSubmitOptions): Promise<void>;
54
+ delete(opts?: FormSubmitOptions): Promise<void>;
55
+ reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
56
+ clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
57
+ clearError(): void;
58
+ };
59
+ //#endregion
60
+ //#region src/runtime/use-navigation.d.ts
61
+ export declare function useNavigation(): NavigationState;
62
+ //#endregion
63
+ //#region src/runtime/use-island-form.d.ts
64
+ type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
65
+ data: T;
66
+ setData: <K extends keyof T & string>(field: K, value: T[K]) => void;
67
+ errors: Record<string, string>;
68
+ error: VoidActionError | null;
69
+ pending: boolean;
70
+ hasChanges: boolean;
71
+ wasSuccessful: boolean;
72
+ recentlySuccessful: boolean;
73
+ reset: (...fields: ReadonlyArray<keyof T & string>) => void;
74
+ clearErrors: (...fields: ReadonlyArray<string>) => void;
75
+ clearError: () => void;
76
+ post: (url: string) => Promise<void>;
77
+ put: (url: string) => Promise<void>;
78
+ patch: (url: string) => Promise<void>;
79
+ delete: (url: string) => Promise<void>;
80
+ }>;
81
+ /**
82
+ * Form composable for island pages. Uses fetch + page reload instead of the
83
+ * Void Router (which is not available in island mode).
84
+ */
85
+ export declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
86
+ //#endregion
87
+ //#region src/runtime/action.d.ts
88
+ type ActionMethod = "POST" | "PUT" | "PATCH" | "DELETE";
89
+ type HasParams<U extends string> = [ResolveActionParams<U>] extends [never] ? false : true;
90
+ type ActionOptions<U extends string> = {
91
+ data?: ResolveActionBody<U>;
92
+ method?: ActionMethod;
93
+ } & (HasParams<U> extends true ? {
94
+ params: ResolveActionParams<U>;
95
+ } : {
96
+ params?: never;
97
+ });
98
+ /** Called during app initialization to store the router instance */
99
+ export declare function setActionRouter(router: VoidRouter): void;
100
+ /** Programmatic one-shot page action call with Inertia page update */
101
+ export declare function action<U extends ActionUrl & string>(url: U, ...args: HasParams<U> extends true ? [options: ActionOptions<U>] : [options?: ActionOptions<U>]): Promise<ActionResult>;
102
+ //#endregion
103
+ export { type Deferred, ErrorsContext, type InferProps, type LinkProps, NavigationContext, ParamsContext, RouterContext, SharedContext };
@@ -0,0 +1,527 @@
1
+ import { ErrorsContext, NavigationContext, ParamsContext, RouterContext, SharedContext } from "./context.mjs";
2
+ import { createMemo, createSignal, onCleanup, onMount, splitProps, useContext } from "solid-js";
3
+ import { VoidActionError, categorizeActionError, idleNavigationState, isAbortError, isEqualFormValue, ssrProxy, submitAction } from "void/pages-client";
4
+ import { createStore, reconcile } from "solid-js/store";
5
+ //#region src/runtime/link.tsx
6
+ function appendQueryValue(params, key, value) {
7
+ if (value === null || value === void 0) return false;
8
+ if (value instanceof Date) {
9
+ params.append(key, value.toISOString());
10
+ return true;
11
+ }
12
+ if (Array.isArray(value)) {
13
+ let appended = false;
14
+ for (const item of value) appended = appendQueryValue(params, key, item) || appended;
15
+ return appended;
16
+ }
17
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
18
+ params.append(key, String(value));
19
+ return true;
20
+ }
21
+ throw new Error(`Link: GET data only supports primitive values and arrays. Remove nested data for '${key}'.`);
22
+ }
23
+ function mergeDataIntoHref(href, data) {
24
+ if (!data) return href;
25
+ const hashIndex = href.indexOf("#");
26
+ const beforeHash = hashIndex === -1 ? href : href.slice(0, hashIndex);
27
+ const hash = hashIndex === -1 ? "" : href.slice(hashIndex);
28
+ const searchIndex = beforeHash.indexOf("?");
29
+ const path = searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex);
30
+ const search = searchIndex === -1 ? "" : beforeHash.slice(searchIndex + 1);
31
+ const params = new URLSearchParams(search);
32
+ let appended = false;
33
+ for (const [key, value] of Object.entries(data)) appended = appendQueryValue(params, key, value) || appended;
34
+ if (!appended) return href;
35
+ const query = params.toString();
36
+ return `${path}${query ? `?${query}` : ""}${hash}`;
37
+ }
38
+ function hasPrefetch(prefetch) {
39
+ return prefetch !== false && prefetch !== void 0;
40
+ }
41
+ function isModifiedEvent(e) {
42
+ const target = e.currentTarget.getAttribute("target");
43
+ return target !== null && target !== "_self" || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || e.button !== 0;
44
+ }
45
+ function mergeStyle(style) {
46
+ if (typeof style === "string") return `touch-action: manipulation; ${style}`;
47
+ return {
48
+ touchAction: "manipulation",
49
+ ...style
50
+ };
51
+ }
52
+ function callHandler(handler, event) {
53
+ if (Array.isArray(handler)) {
54
+ const [fn, data] = handler;
55
+ if (typeof fn === "function") fn(data, event);
56
+ } else if (typeof handler === "function") handler(event);
57
+ }
58
+ function Link(props) {
59
+ const [local, rest] = splitProps(props, [
60
+ "href",
61
+ "method",
62
+ "data",
63
+ "preserveScroll",
64
+ "preserveState",
65
+ "replace",
66
+ "reloadDocument",
67
+ "viewTransition",
68
+ "prefetch",
69
+ "cacheFor",
70
+ "onNavigate",
71
+ "children",
72
+ "style",
73
+ "onClick",
74
+ "onMouseEnter",
75
+ "onMouseLeave",
76
+ "onFocus",
77
+ "onMouseDown",
78
+ "onTouchStart"
79
+ ]);
80
+ const router = useContext(RouterContext);
81
+ const linkState = createMemo(() => {
82
+ const method = (local.method || "GET").toUpperCase();
83
+ const isGet = method === "GET";
84
+ if (hasPrefetch(local.prefetch) && !isGet) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
85
+ if (local.reloadDocument && !isGet) throw new Error("Link: reloadDocument only supports GET links.");
86
+ return {
87
+ href: isGet ? mergeDataIntoHref(local.href, local.data) : local.href,
88
+ isGet,
89
+ method
90
+ };
91
+ });
92
+ const normalizedMethod = () => linkState().method;
93
+ const isGet = () => linkState().isGet;
94
+ const hrefWithData = () => linkState().href;
95
+ const strategies = createMemo(() => {
96
+ if (local.prefetch === false || local.prefetch === void 0 || local.reloadDocument) return [];
97
+ if (local.prefetch === true) return ["hover"];
98
+ if (typeof local.prefetch === "string") return [local.prefetch];
99
+ return local.prefetch;
100
+ });
101
+ const resolvedStyle = createMemo(() => mergeStyle(local.style));
102
+ function doPrefetch() {
103
+ if (!router?.prefetch) return;
104
+ const current = window.location.pathname + window.location.search;
105
+ if (hrefWithData() === current) return;
106
+ const options = {};
107
+ if (local.cacheFor !== void 0) options.cacheFor = local.cacheFor;
108
+ if (normalizedMethod()) options.method = normalizedMethod();
109
+ router.prefetch(hrefWithData(), options);
110
+ }
111
+ function shouldHandleClick(event) {
112
+ if (local.reloadDocument || !router || event.defaultPrevented || isModifiedEvent(event)) return false;
113
+ if (event.currentTarget.hasAttribute("download")) return false;
114
+ if (new URL(hrefWithData(), window.location.origin).origin !== window.location.origin) return false;
115
+ return true;
116
+ }
117
+ function onClick(event) {
118
+ callHandler(local.onClick, event);
119
+ if (!shouldHandleClick(event)) return;
120
+ let navigatePrevented = false;
121
+ local.onNavigate?.({ preventDefault() {
122
+ navigatePrevented = true;
123
+ } });
124
+ if (navigatePrevented) {
125
+ event.preventDefault();
126
+ return;
127
+ }
128
+ event.preventDefault();
129
+ router?.visit(hrefWithData(), {
130
+ method: normalizedMethod(),
131
+ data: isGet() ? void 0 : local.data,
132
+ preserveScroll: local.preserveScroll,
133
+ preserveState: local.preserveState,
134
+ replace: local.replace,
135
+ viewTransition: local.viewTransition
136
+ });
137
+ }
138
+ let hoverTimer = null;
139
+ let observer = null;
140
+ let el;
141
+ function onMouseEnter(event) {
142
+ callHandler(local.onMouseEnter, event);
143
+ if (event.defaultPrevented) return;
144
+ if (hoverTimer) clearTimeout(hoverTimer);
145
+ hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
146
+ }
147
+ function onMouseLeave(event) {
148
+ callHandler(local.onMouseLeave, event);
149
+ if (hoverTimer) {
150
+ clearTimeout(hoverTimer);
151
+ hoverTimer = null;
152
+ }
153
+ }
154
+ function onFocus(event) {
155
+ callHandler(local.onFocus, event);
156
+ if (!event.defaultPrevented) doPrefetch();
157
+ }
158
+ function onMouseDown(event) {
159
+ callHandler(local.onMouseDown, event);
160
+ if (!event.defaultPrevented) doPrefetch();
161
+ }
162
+ function onTouchStart(event) {
163
+ callHandler(local.onTouchStart, event);
164
+ if (!event.defaultPrevented) doPrefetch();
165
+ }
166
+ onMount(() => {
167
+ if (strategies().includes("mount")) doPrefetch();
168
+ if (strategies().includes("visible") && el) {
169
+ observer = new IntersectionObserver((entries) => {
170
+ if (entries.some((e) => e.isIntersecting)) {
171
+ doPrefetch();
172
+ observer?.disconnect();
173
+ observer = null;
174
+ }
175
+ }, { rootMargin: "0px" });
176
+ observer.observe(el);
177
+ }
178
+ });
179
+ onCleanup(() => {
180
+ if (hoverTimer) clearTimeout(hoverTimer);
181
+ if (observer) {
182
+ observer.disconnect();
183
+ observer = null;
184
+ }
185
+ });
186
+ const eventHandlers = createMemo(() => {
187
+ const handlers = {
188
+ onClick,
189
+ onFocus: local.onFocus,
190
+ onMouseDown: local.onMouseDown,
191
+ onMouseEnter: local.onMouseEnter,
192
+ onMouseLeave: local.onMouseLeave,
193
+ onTouchStart: local.onTouchStart
194
+ };
195
+ if (strategies().includes("hover")) {
196
+ handlers.onMouseEnter = onMouseEnter;
197
+ handlers.onMouseLeave = onMouseLeave;
198
+ handlers.onFocus = onFocus;
199
+ handlers.onTouchStart = onTouchStart;
200
+ }
201
+ if (strategies().includes("click")) {
202
+ handlers.onMouseDown = onMouseDown;
203
+ handlers.onTouchStart = onTouchStart;
204
+ }
205
+ return handlers;
206
+ });
207
+ return <>
208
+ {isGet() ? <a ref={(r) => {
209
+ el = r;
210
+ }} href={hrefWithData()} {...rest} style={resolvedStyle()} {...eventHandlers()}>
211
+ {local.children}
212
+ </a> : <button ref={(r) => {
213
+ el = r;
214
+ }} type="button" {...rest} style={resolvedStyle()} {...eventHandlers()}>
215
+ {local.children}
216
+ </button>}
217
+ </>;
218
+ }
219
+ //#endregion
220
+ //#region src/runtime/use-router.ts
221
+ function useRouter() {
222
+ return useContext(RouterContext) ?? ssrProxy;
223
+ }
224
+ //#endregion
225
+ //#region src/runtime/use-params.ts
226
+ function useParams() {
227
+ const params = useContext(ParamsContext);
228
+ if (params === null) throw new Error("useParams(): must be used inside a Void page.");
229
+ return params;
230
+ }
231
+ //#endregion
232
+ //#region src/runtime/use-shared.ts
233
+ function useShared() {
234
+ const shared = useContext(SharedContext);
235
+ if (shared === null) throw new Error("useShared(): must be used inside a pages component.");
236
+ return shared;
237
+ }
238
+ //#endregion
239
+ //#region src/runtime/use-form.ts
240
+ function getValidationErrors$1(error) {
241
+ const body = error.body;
242
+ if (body && typeof body === "object" && "errors" in body && body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)) return body.errors;
243
+ return null;
244
+ }
245
+ function useForm(url, defaults, options) {
246
+ let resolvedUrl = url;
247
+ let actionQuery = "";
248
+ const qIdx = resolvedUrl.indexOf("?");
249
+ if (qIdx !== -1) {
250
+ actionQuery = resolvedUrl.slice(qIdx);
251
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
252
+ }
253
+ if (options?.params) for (const [key, value] of Object.entries(options.params)) resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
254
+ resolvedUrl = resolvedUrl + actionQuery;
255
+ const router = useContext(RouterContext);
256
+ const globalErrors = useContext(ErrorsContext);
257
+ const [data, setDataStore] = createStore({ ...defaults });
258
+ const [localErrors, setLocalErrors] = createSignal({});
259
+ const errors = createMemo(() => {
260
+ const local = localErrors();
261
+ if (Object.keys(local).length > 0) return local;
262
+ if (!globalErrors) return {};
263
+ if (typeof globalErrors === "function") return globalErrors();
264
+ return globalErrors;
265
+ });
266
+ const [pending, setPending] = createSignal(false);
267
+ const [error, setError] = createSignal(null);
268
+ const [wasSuccessful, setWasSuccessful] = createSignal(false);
269
+ const [recentlySuccessful, setRecentlySuccessful] = createSignal(false);
270
+ let defaults_ = { ...defaults };
271
+ let successTimeout = null;
272
+ const hasChanges = createMemo(() => {
273
+ return Object.keys(defaults_).some((key) => !isEqualFormValue(data[key], defaults_[key]));
274
+ });
275
+ function setData(field, value) {
276
+ setDataStore(field, value);
277
+ }
278
+ function reset(...fields) {
279
+ if (fields.length === 0) setDataStore(reconcile({ ...defaults_ }));
280
+ else for (const field of fields) setDataStore(field, defaults_[field]);
281
+ }
282
+ function clearErrors(...fields) {
283
+ if (fields.length === 0) setLocalErrors({});
284
+ else setLocalErrors((prev) => {
285
+ const next = { ...prev };
286
+ for (const field of fields) delete next[field];
287
+ return next;
288
+ });
289
+ }
290
+ function clearError() {
291
+ setError(null);
292
+ }
293
+ async function submit(method, opts = {}) {
294
+ setPending(true);
295
+ setWasSuccessful(false);
296
+ setRecentlySuccessful(false);
297
+ if (successTimeout) clearTimeout(successTimeout);
298
+ setLocalErrors({});
299
+ clearError();
300
+ try {
301
+ if (!router) throw new Error("useForm(): requires the Void Router.");
302
+ const result = await submitAction(router, resolvedUrl, {
303
+ method,
304
+ data: { ...data },
305
+ ...opts
306
+ });
307
+ if (!result.ok) {
308
+ const validationErrors = getValidationErrors$1(result.error);
309
+ if (validationErrors) setLocalErrors(validationErrors);
310
+ else setError(result.error);
311
+ return;
312
+ }
313
+ setWasSuccessful(true);
314
+ setRecentlySuccessful(true);
315
+ defaults_ = { ...data };
316
+ successTimeout = setTimeout(() => {
317
+ setRecentlySuccessful(false);
318
+ }, 2e3);
319
+ } catch (submitError) {
320
+ if (isAbortError(submitError)) return;
321
+ throw submitError;
322
+ } finally {
323
+ setPending(false);
324
+ }
325
+ }
326
+ return {
327
+ get data() {
328
+ return data;
329
+ },
330
+ setData,
331
+ get errors() {
332
+ return errors();
333
+ },
334
+ get error() {
335
+ return error();
336
+ },
337
+ get pending() {
338
+ return pending();
339
+ },
340
+ get hasChanges() {
341
+ return hasChanges();
342
+ },
343
+ get wasSuccessful() {
344
+ return wasSuccessful();
345
+ },
346
+ get recentlySuccessful() {
347
+ return recentlySuccessful();
348
+ },
349
+ reset,
350
+ clearErrors,
351
+ clearError,
352
+ post: (opts) => submit("POST", opts),
353
+ put: (opts) => submit("PUT", opts),
354
+ patch: (opts) => submit("PATCH", opts),
355
+ delete: (opts) => submit("DELETE", opts)
356
+ };
357
+ }
358
+ //#endregion
359
+ //#region src/runtime/use-navigation.ts
360
+ function useNavigation() {
361
+ const navigation = useContext(NavigationContext);
362
+ if (!navigation) return idleNavigationState();
363
+ return {
364
+ get state() {
365
+ return navigation().state;
366
+ },
367
+ get location() {
368
+ return navigation().location;
369
+ },
370
+ get method() {
371
+ return navigation().method;
372
+ }
373
+ };
374
+ }
375
+ //#endregion
376
+ //#region src/runtime/use-island-form.ts
377
+ function getValidationErrors(body) {
378
+ if (body && typeof body === "object" && "errors" in body && body.errors && typeof body.errors === "object" && !Array.isArray(body.errors)) return body.errors;
379
+ return null;
380
+ }
381
+ async function readActionErrorBody(response) {
382
+ const contentType = response.headers.get("content-type") || "";
383
+ try {
384
+ if (contentType.includes("application/json")) return await response.json();
385
+ return await response.text() || null;
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+ /**
391
+ * Form composable for island pages. Uses fetch + page reload instead of the
392
+ * Void Router (which is not available in island mode).
393
+ */
394
+ function useIslandForm(defaults) {
395
+ const [data, setDataStore] = createStore({ ...defaults });
396
+ const [errors, setErrors] = createSignal({});
397
+ const [error, setError] = createSignal(null);
398
+ const [pending, setPending] = createSignal(false);
399
+ const [wasSuccessful, setWasSuccessful] = createSignal(false);
400
+ const [recentlySuccessful, setRecentlySuccessful] = createSignal(false);
401
+ let defaults_ = { ...defaults };
402
+ let successTimeout = null;
403
+ const hasChanges = createMemo(() => {
404
+ return Object.keys(defaults_).some((key) => !isEqualFormValue(data[key], defaults_[key]));
405
+ });
406
+ function setData(field, value) {
407
+ setDataStore(field, value);
408
+ }
409
+ function reset(...fields) {
410
+ if (fields.length === 0) setDataStore(reconcile({ ...defaults_ }));
411
+ else for (const field of fields) setDataStore(field, defaults_[field]);
412
+ }
413
+ function clearErrors(...fields) {
414
+ if (fields.length === 0) setErrors({});
415
+ else setErrors((prev) => {
416
+ const next = { ...prev };
417
+ for (const field of fields) delete next[field];
418
+ return next;
419
+ });
420
+ }
421
+ function clearError() {
422
+ setError(null);
423
+ }
424
+ async function submit(url, method) {
425
+ setPending(true);
426
+ setWasSuccessful(false);
427
+ setRecentlySuccessful(false);
428
+ if (successTimeout) clearTimeout(successTimeout);
429
+ setErrors({});
430
+ clearError();
431
+ try {
432
+ const response = await fetch(url, {
433
+ method,
434
+ headers: { "Content-Type": "application/json" },
435
+ body: JSON.stringify(data)
436
+ });
437
+ if (response.redirected) {
438
+ window.location.href = response.url;
439
+ return;
440
+ }
441
+ if (response.ok) {
442
+ setWasSuccessful(true);
443
+ setRecentlySuccessful(true);
444
+ defaults_ = { ...data };
445
+ successTimeout = setTimeout(() => setRecentlySuccessful(false), 2e3);
446
+ window.location.reload();
447
+ } else if (!response.ok) {
448
+ const body = await readActionErrorBody(response);
449
+ const actionError = new VoidActionError({
450
+ body,
451
+ status: response.status,
452
+ statusText: response.statusText,
453
+ url
454
+ });
455
+ const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
456
+ if (validationErrors) {
457
+ setErrors(validationErrors);
458
+ return;
459
+ }
460
+ if (categorizeActionError(response.status) === "boundary") throw actionError;
461
+ setError(actionError);
462
+ }
463
+ } finally {
464
+ setPending(false);
465
+ }
466
+ }
467
+ return {
468
+ get data() {
469
+ return data;
470
+ },
471
+ setData,
472
+ get errors() {
473
+ return errors();
474
+ },
475
+ get error() {
476
+ return error();
477
+ },
478
+ get pending() {
479
+ return pending();
480
+ },
481
+ get hasChanges() {
482
+ return hasChanges();
483
+ },
484
+ get wasSuccessful() {
485
+ return wasSuccessful();
486
+ },
487
+ get recentlySuccessful() {
488
+ return recentlySuccessful();
489
+ },
490
+ reset,
491
+ clearErrors,
492
+ clearError,
493
+ post: (url) => submit(url, "POST"),
494
+ put: (url) => submit(url, "PUT"),
495
+ patch: (url) => submit(url, "PATCH"),
496
+ delete: (url) => submit(url, "DELETE")
497
+ };
498
+ }
499
+ //#endregion
500
+ //#region src/runtime/action.ts
501
+ let _router = null;
502
+ /** Called during app initialization to store the router instance */
503
+ function setActionRouter(router) {
504
+ _router = router;
505
+ }
506
+ /** Programmatic one-shot page action call with Inertia page update */
507
+ async function action(url, ...args) {
508
+ if (!_router) throw new Error("action(): called before router initialization.");
509
+ const options = args[0];
510
+ const data = options?.data;
511
+ const method = options?.method || "POST";
512
+ let resolvedUrl = url;
513
+ let actionQuery = "";
514
+ const qIdx = resolvedUrl.indexOf("?");
515
+ if (qIdx !== -1) {
516
+ actionQuery = resolvedUrl.slice(qIdx);
517
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
518
+ }
519
+ if (options?.params) for (const [key, value] of Object.entries(options.params)) resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
520
+ resolvedUrl = resolvedUrl + actionQuery;
521
+ return submitAction(_router, resolvedUrl, {
522
+ method,
523
+ data
524
+ });
525
+ }
526
+ //#endregion
527
+ export { ErrorsContext, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/solid",
3
- "version": "0.10.13",
3
+ "version": "0.20.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -13,46 +13,36 @@
13
13
  "type": "module",
14
14
  "exports": {
15
15
  ".": {
16
- "types": "./src/runtime/index.ts",
16
+ "types": "./dist/runtime/index.d.mts",
17
17
  "default": "./src/runtime/index.ts"
18
18
  },
19
19
  "./plugin": {
20
20
  "types": "./dist/plugin.d.mts",
21
21
  "import": "./dist/plugin.mjs"
22
- },
23
- "./runtime": {
24
- "types": "./src/runtime/index.ts",
25
- "default": "./src/runtime/index.ts"
26
- },
27
- "./context": {
28
- "default": "./src/runtime/context.ts"
29
- },
30
- "./prefetch": {
31
- "default": "./src/runtime/prefetch.ts"
32
22
  }
33
23
  },
34
24
  "publishConfig": {
35
25
  "access": "public"
36
26
  },
37
27
  "dependencies": {
38
- "vite-plugin-solid": "^2.11.12"
28
+ "vite-plugin-solid": "^2.11.14"
39
29
  },
40
30
  "devDependencies": {
41
- "@types/node": "^26.1.0",
31
+ "@types/node": "^26.5.1",
42
32
  "pathslash": "^0.1.0",
43
- "solid-js": "^1.9.14",
44
- "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
45
- "vite-plus": "^0.2.2",
46
- "void": "0.10.13"
33
+ "solid-js": "^1.9.15",
34
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
35
+ "vite-plus": "0.3.1",
36
+ "void": "0.20.1"
47
37
  },
48
38
  "peerDependencies": {
49
39
  "solid-js": "^1.9.0",
50
40
  "vite": "^8.0.0",
51
- "void": "0.10.13"
41
+ "void": "0.20.1"
52
42
  },
53
43
  "scripts": {
54
44
  "build": "vp pack",
55
45
  "dev": "vp pack --watch",
56
- "typecheck": "tsgo --noEmit"
46
+ "typecheck": "tsc --noEmit --rootDir ../.."
57
47
  }
58
48
  }
@@ -1,2 +0,0 @@
1
- export { PrefetchCache, parseCacheFor } from 'void/pages-prefetch';
2
- export type { CacheForResult, CacheForInput, CacheEntry } from 'void/pages-prefetch';