@void/solid 0.0.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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@void/solid",
3
+ "version": "0.0.0",
4
+ "files": [
5
+ "dist",
6
+ "src/runtime"
7
+ ],
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/runtime/index.ts",
12
+ "default": "./src/runtime/index.ts"
13
+ },
14
+ "./plugin": {
15
+ "types": "./dist/plugin.d.mts",
16
+ "import": "./dist/plugin.mjs"
17
+ },
18
+ "./runtime": {
19
+ "types": "./src/runtime/index.ts",
20
+ "default": "./src/runtime/index.ts"
21
+ },
22
+ "./context": {
23
+ "default": "./src/runtime/context.ts"
24
+ },
25
+ "./prefetch": {
26
+ "default": "./src/runtime/prefetch.ts"
27
+ }
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "dev": "tsdown --watch",
35
+ "typecheck": "tsgo --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "vite-plugin-solid": "^2.11.12"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "catalog:",
42
+ "pathe": "catalog:",
43
+ "solid-js": "^1.9.12",
44
+ "tsdown": "catalog:",
45
+ "vite": "catalog:",
46
+ "void": "workspace:*"
47
+ },
48
+ "peerDependencies": {
49
+ "solid-js": "^1.9.0",
50
+ "vite": "catalog:",
51
+ "void": "workspace:*"
52
+ }
53
+ }
@@ -0,0 +1,50 @@
1
+ import { submitAction, type ActionResult, type VoidRouter } from 'void/pages-client';
2
+ import type { ActionUrl, ResolveActionBody, ResolveActionParams } from 'void/routes';
3
+
4
+ type ActionMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
+
6
+ type HasParams<U extends string> = [ResolveActionParams<U>] extends [never] ? false : true;
7
+
8
+ type ActionOptions<U extends string> = {
9
+ data?: ResolveActionBody<U>;
10
+ method?: ActionMethod;
11
+ } & (HasParams<U> extends true ? { params: ResolveActionParams<U> } : { params?: never });
12
+
13
+ let _router: VoidRouter | null = null;
14
+
15
+ /** Called during app initialization to store the router instance */
16
+ export function setActionRouter(router: VoidRouter): void {
17
+ _router = router;
18
+ }
19
+
20
+ /** Programmatic one-shot page action call with Inertia page update */
21
+ export async function action<U extends ActionUrl & string>(
22
+ url: U,
23
+ ...args: HasParams<U> extends true ? [options: ActionOptions<U>] : [options?: ActionOptions<U>]
24
+ ): Promise<ActionResult> {
25
+ if (!_router) {
26
+ throw new Error('action(): called before router initialization.');
27
+ }
28
+
29
+ const options = args[0] as ActionOptions<U> | undefined;
30
+ const data = options?.data;
31
+ const method = options?.method || 'POST';
32
+
33
+ let resolvedUrl: string = url;
34
+ let actionQuery = '';
35
+
36
+ const qIdx = resolvedUrl.indexOf('?');
37
+ if (qIdx !== -1) {
38
+ actionQuery = resolvedUrl.slice(qIdx);
39
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
40
+ }
41
+
42
+ if (options?.params) {
43
+ for (const [key, value] of Object.entries(options.params)) {
44
+ resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value as string));
45
+ }
46
+ }
47
+
48
+ resolvedUrl = resolvedUrl + actionQuery;
49
+ return submitAction(_router, resolvedUrl, { method, data });
50
+ }
@@ -0,0 +1,14 @@
1
+ import { createContext, type Context } from 'solid-js';
2
+ import type { NavigationState, VoidRouter } from 'void/pages-client';
3
+
4
+ export const SharedContext: Context<Record<string, unknown> | null> = createContext<Record<
5
+ string,
6
+ unknown
7
+ > | null>(null);
8
+ export const ErrorsContext: Context<
9
+ Record<string, string> | (() => Record<string, string>) | null
10
+ > = createContext<Record<string, string> | (() => Record<string, string>) | null>(null);
11
+ export const RouterContext: Context<VoidRouter | null> = createContext<VoidRouter | null>(null);
12
+ export const NavigationContext: Context<(() => NavigationState) | null> = createContext<
13
+ (() => NavigationState) | null
14
+ >(null);
@@ -0,0 +1,9 @@
1
+ export { Link, type LinkProps } from './link.tsx';
2
+ export { useRouter } from './use-router.ts';
3
+ export { useShared } from './use-shared.ts';
4
+ export { useForm } from './use-form.ts';
5
+ export { useNavigation } from './use-navigation.ts';
6
+ export { useIslandForm } from './use-island-form.ts';
7
+ export { action, setActionRouter } from './action.ts';
8
+ export { SharedContext, ErrorsContext, RouterContext, NavigationContext } from './context.ts';
9
+ export type { DeferredState as Deferred, InferProps } from 'void';
@@ -0,0 +1,365 @@
1
+ import { createMemo, onCleanup, onMount, splitProps, useContext } from 'solid-js';
2
+ import type { JSX } from 'solid-js';
3
+ import { RouterContext } from './context.ts';
4
+
5
+ type PrefetchStrategy = 'hover' | 'click' | 'mount' | 'visible';
6
+ type LinkMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
7
+ type NavigateEvent = { preventDefault(): void };
8
+ type PrefetchOptions = { cacheFor?: LinkProps['cacheFor']; method?: LinkMethod };
9
+
10
+ type CommonProps = {
11
+ href: string;
12
+ method?: LinkMethod | Lowercase<LinkMethod>;
13
+ data?: Record<string, unknown>;
14
+ preserveScroll?: boolean;
15
+ preserveState?: boolean;
16
+ replace?: boolean;
17
+ reloadDocument?: boolean;
18
+ viewTransition?: boolean;
19
+ prefetch?: boolean | PrefetchStrategy | Array<PrefetchStrategy>;
20
+ cacheFor?: number | string | [string, string];
21
+ onNavigate?: (event: NavigateEvent) => void;
22
+ children?: JSX.Element;
23
+ };
24
+
25
+ export type LinkProps = CommonProps &
26
+ Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, keyof CommonProps> &
27
+ Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, keyof CommonProps>;
28
+
29
+ function appendQueryValue(params: URLSearchParams, key: string, value: unknown): boolean {
30
+ if (value === null || value === undefined) {
31
+ return false;
32
+ }
33
+ if (value instanceof Date) {
34
+ params.append(key, value.toISOString());
35
+ return true;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ let appended = false;
39
+ for (const item of value) {
40
+ appended = appendQueryValue(params, key, item) || appended;
41
+ }
42
+ return appended;
43
+ }
44
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
45
+ params.append(key, String(value));
46
+ return true;
47
+ }
48
+ throw new Error(
49
+ `Link: GET data only supports primitive values and arrays. Remove nested data for '${key}'.`,
50
+ );
51
+ }
52
+
53
+ function mergeDataIntoHref(href: string, data?: Record<string, unknown>): string {
54
+ if (!data) {
55
+ return href;
56
+ }
57
+
58
+ const hashIndex = href.indexOf('#');
59
+ const beforeHash = hashIndex === -1 ? href : href.slice(0, hashIndex);
60
+ const hash = hashIndex === -1 ? '' : href.slice(hashIndex);
61
+ const searchIndex = beforeHash.indexOf('?');
62
+ const path = searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex);
63
+ const search = searchIndex === -1 ? '' : beforeHash.slice(searchIndex + 1);
64
+ const params = new URLSearchParams(search);
65
+ let appended = false;
66
+
67
+ for (const [key, value] of Object.entries(data)) {
68
+ appended = appendQueryValue(params, key, value) || appended;
69
+ }
70
+
71
+ if (!appended) {
72
+ return href;
73
+ }
74
+
75
+ const query = params.toString();
76
+ return `${path}${query ? `?${query}` : ''}${hash}`;
77
+ }
78
+
79
+ function hasPrefetch(prefetch: LinkProps['prefetch']): boolean {
80
+ return prefetch !== false && prefetch !== undefined;
81
+ }
82
+
83
+ function isModifiedEvent(e: MouseEvent): boolean {
84
+ const target = (e.currentTarget as HTMLElement).getAttribute('target');
85
+ return (
86
+ (target !== null && target !== '_self') ||
87
+ e.ctrlKey ||
88
+ e.metaKey ||
89
+ e.shiftKey ||
90
+ e.altKey ||
91
+ e.button !== 0
92
+ );
93
+ }
94
+
95
+ function mergeStyle(style: JSX.CSSProperties | string | undefined): JSX.CSSProperties | string {
96
+ if (typeof style === 'string') {
97
+ return `touch-action: manipulation; ${style}`;
98
+ }
99
+ return {
100
+ touchAction: 'manipulation',
101
+ ...style,
102
+ } as JSX.CSSProperties;
103
+ }
104
+
105
+ function callHandler(handler: unknown, event: Event): void {
106
+ if (Array.isArray(handler)) {
107
+ const [fn, data] = handler;
108
+ if (typeof fn === 'function') {
109
+ fn(data, event);
110
+ }
111
+ } else if (typeof handler === 'function') {
112
+ handler(event);
113
+ }
114
+ }
115
+
116
+ export function Link(props: LinkProps): JSX.Element {
117
+ const [local, rest] = splitProps(props, [
118
+ 'href',
119
+ 'method',
120
+ 'data',
121
+ 'preserveScroll',
122
+ 'preserveState',
123
+ 'replace',
124
+ 'reloadDocument',
125
+ 'viewTransition',
126
+ 'prefetch',
127
+ 'cacheFor',
128
+ 'onNavigate',
129
+ 'children',
130
+ 'style',
131
+ 'onClick',
132
+ 'onMouseEnter',
133
+ 'onMouseLeave',
134
+ 'onFocus',
135
+ 'onMouseDown',
136
+ 'onTouchStart',
137
+ ]);
138
+ const router = useContext(RouterContext);
139
+
140
+ const linkState = createMemo(() => {
141
+ const method = (local.method || 'GET').toUpperCase() as LinkMethod;
142
+ const isGet = method === 'GET';
143
+
144
+ if (hasPrefetch(local.prefetch) && !isGet) {
145
+ throw new Error(
146
+ 'Link: prefetch only supports GET links. Remove `prefetch` or use method="GET".',
147
+ );
148
+ }
149
+ if (local.reloadDocument && !isGet) {
150
+ throw new Error('Link: reloadDocument only supports GET links.');
151
+ }
152
+
153
+ return {
154
+ href: isGet ? mergeDataIntoHref(local.href, local.data) : local.href,
155
+ isGet,
156
+ method,
157
+ };
158
+ });
159
+ const normalizedMethod = () => linkState().method;
160
+ const isGet = () => linkState().isGet;
161
+ const hrefWithData = () => linkState().href;
162
+ const strategies = createMemo<Array<PrefetchStrategy>>(() => {
163
+ if (local.prefetch === false || local.prefetch === undefined || local.reloadDocument) {
164
+ return [];
165
+ }
166
+ if (local.prefetch === true) {
167
+ return ['hover'];
168
+ }
169
+ if (typeof local.prefetch === 'string') {
170
+ return [local.prefetch];
171
+ }
172
+ return local.prefetch;
173
+ });
174
+ const resolvedStyle = createMemo(() => mergeStyle(local.style));
175
+
176
+ function doPrefetch() {
177
+ if (!router?.prefetch) {
178
+ return;
179
+ }
180
+ const current = window.location.pathname + window.location.search;
181
+ if (hrefWithData() === current) {
182
+ return;
183
+ }
184
+ const options: PrefetchOptions = {};
185
+ if (local.cacheFor !== undefined) {
186
+ options.cacheFor = local.cacheFor;
187
+ }
188
+ if (normalizedMethod()) {
189
+ options.method = normalizedMethod();
190
+ }
191
+ router.prefetch(hrefWithData(), options);
192
+ }
193
+
194
+ function shouldHandleClick(event: MouseEvent): boolean {
195
+ if (local.reloadDocument || !router || event.defaultPrevented || isModifiedEvent(event)) {
196
+ return false;
197
+ }
198
+ if ((event.currentTarget as HTMLElement).hasAttribute('download')) {
199
+ return false;
200
+ }
201
+
202
+ const url = new URL(hrefWithData(), window.location.origin);
203
+ if (url.origin !== window.location.origin) {
204
+ return false;
205
+ }
206
+
207
+ return true;
208
+ }
209
+
210
+ function onClick(event: MouseEvent) {
211
+ callHandler(local.onClick, event);
212
+ if (!shouldHandleClick(event)) {
213
+ return;
214
+ }
215
+
216
+ let navigatePrevented = false;
217
+ local.onNavigate?.({
218
+ preventDefault() {
219
+ navigatePrevented = true;
220
+ },
221
+ });
222
+ if (navigatePrevented) {
223
+ event.preventDefault();
224
+ return;
225
+ }
226
+
227
+ event.preventDefault();
228
+ router?.visit(hrefWithData(), {
229
+ method: normalizedMethod(),
230
+ data: isGet() ? undefined : local.data,
231
+ preserveScroll: local.preserveScroll,
232
+ preserveState: local.preserveState,
233
+ replace: local.replace,
234
+ viewTransition: local.viewTransition,
235
+ });
236
+ }
237
+
238
+ let hoverTimer: ReturnType<typeof setTimeout> | null = null;
239
+ let observer: IntersectionObserver | null = null;
240
+ let el: HTMLElement | undefined;
241
+
242
+ function onMouseEnter(event: MouseEvent) {
243
+ callHandler(local.onMouseEnter, event);
244
+ if (event.defaultPrevented) {
245
+ return;
246
+ }
247
+ if (hoverTimer) {
248
+ clearTimeout(hoverTimer);
249
+ }
250
+ hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
251
+ }
252
+
253
+ function onMouseLeave(event: MouseEvent) {
254
+ callHandler(local.onMouseLeave, event);
255
+ if (hoverTimer) {
256
+ clearTimeout(hoverTimer);
257
+ hoverTimer = null;
258
+ }
259
+ }
260
+
261
+ function onFocus(event: FocusEvent) {
262
+ callHandler(local.onFocus, event);
263
+ if (!event.defaultPrevented) {
264
+ doPrefetch();
265
+ }
266
+ }
267
+
268
+ function onMouseDown(event: MouseEvent) {
269
+ callHandler(local.onMouseDown, event);
270
+ if (!event.defaultPrevented) {
271
+ doPrefetch();
272
+ }
273
+ }
274
+
275
+ function onTouchStart(event: TouchEvent) {
276
+ callHandler(local.onTouchStart, event);
277
+ if (!event.defaultPrevented) {
278
+ doPrefetch();
279
+ }
280
+ }
281
+
282
+ onMount(() => {
283
+ if (strategies().includes('mount')) {
284
+ doPrefetch();
285
+ }
286
+ if (strategies().includes('visible') && el) {
287
+ observer = new IntersectionObserver(
288
+ (entries) => {
289
+ if (entries.some((e) => e.isIntersecting)) {
290
+ doPrefetch();
291
+ observer?.disconnect();
292
+ observer = null;
293
+ }
294
+ },
295
+ { rootMargin: '0px' },
296
+ );
297
+ observer.observe(el);
298
+ }
299
+ });
300
+
301
+ onCleanup(() => {
302
+ if (hoverTimer) {
303
+ clearTimeout(hoverTimer);
304
+ }
305
+ if (observer) {
306
+ observer.disconnect();
307
+ observer = null;
308
+ }
309
+ });
310
+
311
+ const eventHandlers = createMemo(() => {
312
+ const handlers: Record<string, unknown> = {
313
+ onClick,
314
+ onFocus: local.onFocus,
315
+ onMouseDown: local.onMouseDown,
316
+ onMouseEnter: local.onMouseEnter,
317
+ onMouseLeave: local.onMouseLeave,
318
+ onTouchStart: local.onTouchStart,
319
+ };
320
+
321
+ if (strategies().includes('hover')) {
322
+ handlers.onMouseEnter = onMouseEnter;
323
+ handlers.onMouseLeave = onMouseLeave;
324
+ handlers.onFocus = onFocus;
325
+ handlers.onTouchStart = onTouchStart;
326
+ }
327
+
328
+ if (strategies().includes('click')) {
329
+ handlers.onMouseDown = onMouseDown;
330
+ handlers.onTouchStart = onTouchStart;
331
+ }
332
+
333
+ return handlers;
334
+ });
335
+
336
+ return (
337
+ <>
338
+ {isGet() ? (
339
+ <a
340
+ ref={(r) => {
341
+ el = r;
342
+ }}
343
+ href={hrefWithData()}
344
+ {...rest}
345
+ style={resolvedStyle()}
346
+ {...eventHandlers()}
347
+ >
348
+ {local.children}
349
+ </a>
350
+ ) : (
351
+ <button
352
+ ref={(r) => {
353
+ el = r;
354
+ }}
355
+ type="button"
356
+ {...(rest as JSX.ButtonHTMLAttributes<HTMLButtonElement>)}
357
+ style={resolvedStyle()}
358
+ {...eventHandlers()}
359
+ >
360
+ {local.children}
361
+ </button>
362
+ )}
363
+ </>
364
+ );
365
+ }
@@ -0,0 +1,2 @@
1
+ export { PrefetchCache, parseCacheFor } from 'void/pages-prefetch';
2
+ export type { CacheForResult, CacheForInput, CacheEntry } from 'void/pages-prefetch';