@void/solid 0.10.13 → 0.20.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/LICENSE +21 -0
- package/README.md +2 -1
- package/dist/plugin.d.mts +3 -5
- package/dist/plugin.mjs +5 -4
- package/dist/runtime/context.d.mts +9 -0
- package/dist/runtime/context.mjs +9 -0
- package/dist/runtime/index.d.mts +103 -0
- package/dist/runtime/index.mjs +527 -0
- package/package.json +10 -20
- package/src/runtime/prefetch.ts +0 -2
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 = "
|
|
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 ["\
|
|
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 === "\
|
|
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 === "\
|
|
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.
|
|
3
|
+
"version": "0.20.0",
|
|
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": "./
|
|
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.
|
|
28
|
+
"vite-plugin-solid": "^2.11.14"
|
|
39
29
|
},
|
|
40
30
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^26.1
|
|
31
|
+
"@types/node": "^26.5.1",
|
|
42
32
|
"pathslash": "^0.1.0",
|
|
43
|
-
"solid-js": "^1.9.
|
|
44
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.
|
|
45
|
-
"vite-plus": "
|
|
46
|
-
"void": "0.
|
|
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.0"
|
|
47
37
|
},
|
|
48
38
|
"peerDependencies": {
|
|
49
39
|
"solid-js": "^1.9.0",
|
|
50
40
|
"vite": "^8.0.0",
|
|
51
|
-
"void": "0.
|
|
41
|
+
"void": "0.20.0"
|
|
52
42
|
},
|
|
53
43
|
"scripts": {
|
|
54
44
|
"build": "vp pack",
|
|
55
45
|
"dev": "vp pack --watch",
|
|
56
|
-
"typecheck": "
|
|
46
|
+
"typecheck": "tsc --noEmit --rootDir ../.."
|
|
57
47
|
}
|
|
58
48
|
}
|
package/src/runtime/prefetch.ts
DELETED