@void/vue 0.26.1 → 0.28.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 CHANGED
@@ -1,2 +1,72 @@
1
- import { a as useIslandForm, c as useShared, d as Link, f as Form, i as setActionRouter, l as useParams, n as InferProps, o as useNavigation, r as action, s as useForm, t as Deferred, u as useRouter } from "./index-vAbysUNV.mjs";
2
- export { type Deferred, Form, type InferProps, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
1
+ import { ActionClient, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
2
+ import { Component } from "vue";
3
+ import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
4
+ import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
5
+ //#region src/runtime/form.d.ts
6
+ export declare const Form: Component;
7
+ //#endregion
8
+ //#region src/runtime/link.d.ts
9
+ export declare const Link: Component;
10
+ //#endregion
11
+ //#region src/runtime/use-router.d.ts
12
+ export declare function useRouter(): VoidRouter;
13
+ //#endregion
14
+ //#region src/runtime/use-params.d.ts
15
+ export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
16
+ //#endregion
17
+ //#region src/runtime/use-shared.d.ts
18
+ export declare function useShared<T = CloudContextVariables["shared"]>(): T;
19
+ //#endregion
20
+ //#region src/runtime/use-form.d.ts
21
+ type FormSubmitOptions = Omit<VisitOptions, "method" | "data"> & {
22
+ data?: Record<string, unknown>;
23
+ };
24
+ export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
25
+ data: ResolveActionBody<U>;
26
+ errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
27
+ error: VoidActionError | null;
28
+ pending: boolean;
29
+ hasChanges: boolean;
30
+ wasSuccessful: boolean;
31
+ recentlySuccessful: boolean;
32
+ post(opts?: FormSubmitOptions): Promise<void>;
33
+ put(opts?: FormSubmitOptions): Promise<void>;
34
+ patch(opts?: FormSubmitOptions): Promise<void>;
35
+ delete(opts?: FormSubmitOptions): Promise<void>;
36
+ reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
37
+ clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
38
+ clearError(): void;
39
+ };
40
+ //#endregion
41
+ //#region src/runtime/use-navigation.d.ts
42
+ export declare function useNavigation(): NavigationState;
43
+ //#endregion
44
+ //#region src/runtime/use-island-form.d.ts
45
+ type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
46
+ data: T;
47
+ errors: Record<string, string>;
48
+ error: VoidActionError | null;
49
+ pending: boolean;
50
+ hasChanges: boolean;
51
+ wasSuccessful: boolean;
52
+ recentlySuccessful: boolean;
53
+ submit(url: string, method: string): Promise<void>;
54
+ post(url: string): Promise<void>;
55
+ put(url: string): Promise<void>;
56
+ patch(url: string): Promise<void>;
57
+ delete(url: string): Promise<void>;
58
+ reset(...fields: ReadonlyArray<keyof T & string>): void;
59
+ clearErrors(...fields: ReadonlyArray<string>): void;
60
+ clearError(): void;
61
+ }>;
62
+ /**
63
+ * Form composable for island pages. Uses fetch + page reload instead of the
64
+ * Void Router (which is not available in island mode).
65
+ */
66
+ export declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
67
+ //#endregion
68
+ //#region src/runtime/action.d.ts
69
+ export declare const action: ActionClient["action"];
70
+ export declare const setActionRouter: ActionClient["setActionRouter"];
71
+ //#endregion
72
+ export type { Deferred, InferProps };
package/dist/index.mjs CHANGED
@@ -1,2 +1,432 @@
1
- import { a as useForm, c as useRouter, i as useNavigation, l as Link, n as setActionRouter, o as useShared, r as useIslandForm, s as useParams, t as action, u as Form } from "./runtime-lPB1mgy1.mjs";
1
+ import { VoidActionError, categorizeActionError, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
2
+ import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, watch } from "vue";
3
+ //#region src/runtime/form.ts
4
+ const Form = defineComponent({
5
+ name: "VoidForm",
6
+ inheritAttrs: false,
7
+ props: {
8
+ form: {
9
+ type: Object,
10
+ required: true
11
+ },
12
+ method: {
13
+ type: String,
14
+ default: "post"
15
+ },
16
+ renderError: Function
17
+ },
18
+ setup(props, { attrs, slots }) {
19
+ const now = ref(Date.now());
20
+ let cancel = () => {};
21
+ onMounted(() => watch(() => props.form.error, (error) => {
22
+ cancel();
23
+ cancel = scheduleFormRetry(error, () => {
24
+ now.value = Date.now();
25
+ });
26
+ }, { immediate: true }));
27
+ onUnmounted(() => cancel());
28
+ return () => {
29
+ const form = props.form;
30
+ const message = formErrorMessage(form), retry = formRetryMessage(form.error);
31
+ return h("form", {
32
+ ...attrs,
33
+ "aria-busy": form.pending,
34
+ onSubmit: async (event) => {
35
+ event.preventDefault();
36
+ if (!formSubmissionBlocked(form, now.value)) await form[props.method]();
37
+ }
38
+ }, [message ? h("div", {
39
+ role: "alert",
40
+ "data-void-form-error": ""
41
+ }, [form.error && props.renderError ? props.renderError(form.error) : message, retry ? h("p", retry) : null]) : null, slots.default?.()]);
42
+ };
43
+ }
44
+ });
45
+ //#endregion
46
+ //#region src/runtime/link.ts
47
+ function callHandler(handler, event) {
48
+ if (Array.isArray(handler)) {
49
+ for (const fn of handler) if (typeof fn === "function") fn(event);
50
+ } else if (typeof handler === "function") handler(event);
51
+ }
52
+ const Link = defineComponent({
53
+ name: "Link",
54
+ props: {
55
+ href: {
56
+ type: String,
57
+ required: true
58
+ },
59
+ method: {
60
+ type: String,
61
+ default: "GET"
62
+ },
63
+ data: {
64
+ type: Object,
65
+ default: void 0
66
+ },
67
+ preserveScroll: {
68
+ type: Boolean,
69
+ default: false
70
+ },
71
+ preserveState: {
72
+ type: Boolean,
73
+ default: false
74
+ },
75
+ replace: {
76
+ type: Boolean,
77
+ default: false
78
+ },
79
+ reloadDocument: {
80
+ type: Boolean,
81
+ default: false
82
+ },
83
+ viewTransition: {
84
+ type: Boolean,
85
+ default: void 0
86
+ },
87
+ onNavigate: {
88
+ type: Function,
89
+ default: void 0
90
+ },
91
+ prefetch: {
92
+ type: [
93
+ Boolean,
94
+ String,
95
+ Array
96
+ ],
97
+ default: false
98
+ },
99
+ cacheFor: {
100
+ type: [
101
+ Number,
102
+ String,
103
+ Array
104
+ ],
105
+ default: void 0
106
+ }
107
+ },
108
+ setup(props, { slots, attrs }) {
109
+ const router = inject("__void_router", null);
110
+ const elRef = ref(null);
111
+ const normalizedMethod = computed(() => props.method.toUpperCase());
112
+ const isGet = computed(() => normalizedMethod.value === "GET");
113
+ const hrefWithData = computed(() => isGet.value ? mergeDataIntoHref(props.href, props.data) : props.href);
114
+ const strategies = computed(() => {
115
+ if (props.prefetch === false || props.reloadDocument) return [];
116
+ if (props.prefetch === true) return ["hover"];
117
+ if (typeof props.prefetch === "string") return [props.prefetch];
118
+ return props.prefetch;
119
+ });
120
+ function doPrefetch() {
121
+ if (!router?.prefetch) return;
122
+ const current = window.location.pathname + window.location.search;
123
+ if (hrefWithData.value === current) return;
124
+ const options = { method: normalizedMethod.value };
125
+ if (props.cacheFor !== void 0) options.cacheFor = props.cacheFor;
126
+ router.prefetch(hrefWithData.value, options);
127
+ }
128
+ function onClick(e) {
129
+ callHandler(attrs.onClick, e);
130
+ if (props.reloadDocument || !router || e.defaultPrevented || isModifiedEvent(e)) return;
131
+ if (e.currentTarget.hasAttribute("download")) return;
132
+ if (new URL(hrefWithData.value, window.location.origin).origin !== window.location.origin) return;
133
+ let navigatePrevented = false;
134
+ props.onNavigate?.({ preventDefault() {
135
+ navigatePrevented = true;
136
+ } });
137
+ if (navigatePrevented) {
138
+ e.preventDefault();
139
+ return;
140
+ }
141
+ e.preventDefault();
142
+ router.visit(hrefWithData.value, {
143
+ method: normalizedMethod.value,
144
+ data: isGet.value ? void 0 : props.data,
145
+ preserveScroll: props.preserveScroll,
146
+ preserveState: props.preserveState,
147
+ replace: props.replace,
148
+ viewTransition: props.viewTransition
149
+ });
150
+ }
151
+ let hoverTimer = null;
152
+ let observer = null;
153
+ function onMouseEnter(e) {
154
+ callHandler(attrs.onMouseenter ?? attrs.onMouseEnter, e);
155
+ if (e.defaultPrevented) return;
156
+ hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
157
+ }
158
+ function onMouseLeave(e) {
159
+ callHandler(attrs.onMouseleave ?? attrs.onMouseLeave, e);
160
+ if (hoverTimer) {
161
+ clearTimeout(hoverTimer);
162
+ hoverTimer = null;
163
+ }
164
+ }
165
+ function onFocus(e) {
166
+ callHandler(attrs.onFocus, e);
167
+ if (!e.defaultPrevented) doPrefetch();
168
+ }
169
+ function onMouseDown(e) {
170
+ callHandler(attrs.onMousedown ?? attrs.onMouseDown, e);
171
+ if (!e.defaultPrevented) doPrefetch();
172
+ }
173
+ function onTouchStart(e) {
174
+ callHandler(attrs.onTouchstart ?? attrs.onTouchStart, e);
175
+ if (!e.defaultPrevented) doPrefetch();
176
+ }
177
+ onMounted(() => {
178
+ if (strategies.value.includes("mount")) doPrefetch();
179
+ if (strategies.value.includes("visible") && elRef.value) {
180
+ observer = new IntersectionObserver((entries) => {
181
+ if (entries.some((e) => e.isIntersecting)) {
182
+ doPrefetch();
183
+ observer?.disconnect();
184
+ observer = null;
185
+ }
186
+ }, { rootMargin: "0px" });
187
+ observer.observe(elRef.value);
188
+ }
189
+ });
190
+ onUnmounted(() => {
191
+ if (hoverTimer) clearTimeout(hoverTimer);
192
+ if (observer) {
193
+ observer.disconnect();
194
+ observer = null;
195
+ }
196
+ });
197
+ return () => {
198
+ if (props.prefetch !== false && !isGet.value) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
199
+ if (props.reloadDocument && !isGet.value) throw new Error("Link: reloadDocument only supports GET links.");
200
+ const tag = isGet.value ? "a" : "button";
201
+ const eventHandlers = { onClick };
202
+ if (strategies.value.includes("hover")) {
203
+ eventHandlers.onMouseenter = onMouseEnter;
204
+ eventHandlers.onMouseleave = onMouseLeave;
205
+ eventHandlers.onFocus = onFocus;
206
+ eventHandlers.onTouchstart = onTouchStart;
207
+ }
208
+ if (strategies.value.includes("click")) {
209
+ eventHandlers.onMousedown = onMouseDown;
210
+ eventHandlers.onTouchstart = onTouchStart;
211
+ }
212
+ return h(tag, {
213
+ ...attrs,
214
+ ref: elRef,
215
+ ...isGet.value ? { href: hrefWithData.value } : { type: "button" },
216
+ style: [{ touchAction: "manipulation" }, attrs.style],
217
+ ...eventHandlers
218
+ }, slots.default?.());
219
+ };
220
+ }
221
+ });
222
+ //#endregion
223
+ //#region src/runtime/use-router.ts
224
+ function useRouter() {
225
+ return inject("__void_router", null) ?? ssrProxy;
226
+ }
227
+ //#endregion
228
+ //#region src/runtime/use-params.ts
229
+ function useParams() {
230
+ const params = inject("__void_params");
231
+ if (!params) throw new Error("useParams(): must be used inside a Void page.");
232
+ return params;
233
+ }
234
+ //#endregion
235
+ //#region src/runtime/use-shared.ts
236
+ function useShared() {
237
+ const shared = inject("__void_shared");
238
+ if (!shared) throw new Error("useShared(): must be used inside a Void page.");
239
+ return shared;
240
+ }
241
+ //#endregion
242
+ //#region src/runtime/use-form.ts
243
+ function useForm(url, defaults, options) {
244
+ const formDefaults = defaults;
245
+ const router = inject("__void_router", null);
246
+ const data = reactive({ ...formDefaults });
247
+ const errors = reactive({});
248
+ const error = ref(null);
249
+ const pending = ref(false);
250
+ const wasSuccessful = ref(false);
251
+ const recentlySuccessful = ref(false);
252
+ let defaults_ = { ...formDefaults };
253
+ let successTimeout = null;
254
+ const hasChanges = ref(false);
255
+ watch(() => ({ ...data }), (val) => {
256
+ hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
257
+ }, { deep: true });
258
+ function reset(...fields) {
259
+ if (fields.length === 0) Object.assign(data, { ...defaults_ });
260
+ else for (const field of fields) data[field] = defaults_[field];
261
+ }
262
+ function clearErrors(...fields) {
263
+ if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
264
+ else for (const field of fields) delete errors[field];
265
+ }
266
+ function clearError() {
267
+ error.value = null;
268
+ }
269
+ async function submit(method, opts = {}) {
270
+ pending.value = true;
271
+ wasSuccessful.value = false;
272
+ recentlySuccessful.value = false;
273
+ if (successTimeout) clearTimeout(successTimeout);
274
+ clearErrors();
275
+ clearError();
276
+ try {
277
+ if (!router) throw new Error("useForm(): requires the Void Router.");
278
+ const result = await submitAction(router, interpolateRouteParams(url, options?.params), {
279
+ method,
280
+ data: { ...data },
281
+ ...opts
282
+ });
283
+ if (!result.ok) {
284
+ const validationErrors = getValidationErrors(result.error.body);
285
+ if (validationErrors) Object.assign(errors, validationErrors);
286
+ else error.value = result.error;
287
+ return;
288
+ }
289
+ wasSuccessful.value = true;
290
+ recentlySuccessful.value = true;
291
+ defaults_ = { ...data };
292
+ successTimeout = setTimeout(() => {
293
+ recentlySuccessful.value = false;
294
+ }, 2e3);
295
+ } catch (submitError) {
296
+ if (isAbortError(submitError)) return;
297
+ throw submitError;
298
+ } finally {
299
+ pending.value = false;
300
+ }
301
+ }
302
+ return reactive({
303
+ data,
304
+ errors,
305
+ error,
306
+ pending,
307
+ hasChanges,
308
+ wasSuccessful,
309
+ recentlySuccessful,
310
+ reset,
311
+ clearErrors,
312
+ clearError,
313
+ post: (opts) => submit("POST", opts),
314
+ put: (opts) => submit("PUT", opts),
315
+ patch: (opts) => submit("PATCH", opts),
316
+ delete: (opts) => submit("DELETE", opts)
317
+ });
318
+ }
319
+ //#endregion
320
+ //#region src/runtime/use-navigation.ts
321
+ function useNavigation() {
322
+ return inject("__void_navigation", null) ?? idleNavigationState();
323
+ }
324
+ //#endregion
325
+ //#region src/runtime/use-island-form.ts
326
+ async function readActionErrorBody(response) {
327
+ const contentType = response.headers.get("content-type") || "";
328
+ try {
329
+ if (contentType.includes("application/json")) return await response.json();
330
+ return await response.text() || null;
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+ /**
336
+ * Form composable for island pages. Uses fetch + page reload instead of the
337
+ * Void Router (which is not available in island mode).
338
+ */
339
+ function useIslandForm(defaults) {
340
+ const data = reactive({ ...defaults });
341
+ const errors = reactive({});
342
+ const error = ref(null);
343
+ const pending = ref(false);
344
+ const wasSuccessful = ref(false);
345
+ const recentlySuccessful = ref(false);
346
+ let defaults_ = { ...defaults };
347
+ let successTimeout = null;
348
+ const hasChanges = ref(false);
349
+ watch(() => ({ ...data }), (val) => {
350
+ hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
351
+ }, { deep: true });
352
+ function reset(...fields) {
353
+ if (fields.length === 0) Object.assign(data, { ...defaults_ });
354
+ else for (const field of fields) data[field] = defaults_[field];
355
+ }
356
+ function clearErrors(...fields) {
357
+ if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
358
+ else for (const field of fields) delete errors[field];
359
+ }
360
+ function clearError() {
361
+ error.value = null;
362
+ }
363
+ async function submit(url, method) {
364
+ pending.value = true;
365
+ wasSuccessful.value = false;
366
+ recentlySuccessful.value = false;
367
+ if (successTimeout) clearTimeout(successTimeout);
368
+ clearErrors();
369
+ clearError();
370
+ try {
371
+ const response = await fetch(url, {
372
+ method,
373
+ headers: { "Content-Type": "application/json" },
374
+ body: JSON.stringify(data)
375
+ });
376
+ if (response.redirected) {
377
+ window.location.href = response.url;
378
+ return;
379
+ }
380
+ if (response.ok) {
381
+ wasSuccessful.value = true;
382
+ recentlySuccessful.value = true;
383
+ defaults_ = { ...data };
384
+ successTimeout = setTimeout(() => {
385
+ recentlySuccessful.value = false;
386
+ }, 2e3);
387
+ window.location.reload();
388
+ } else {
389
+ const body = await readActionErrorBody(response);
390
+ const actionError = new VoidActionError({
391
+ body,
392
+ status: response.status,
393
+ statusText: response.statusText,
394
+ url
395
+ });
396
+ const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
397
+ if (validationErrors) {
398
+ Object.assign(errors, validationErrors);
399
+ return;
400
+ }
401
+ if (categorizeActionError(response.status) === "boundary") throw actionError;
402
+ error.value = actionError;
403
+ }
404
+ } finally {
405
+ pending.value = false;
406
+ }
407
+ }
408
+ return reactive({
409
+ data,
410
+ errors,
411
+ error,
412
+ pending,
413
+ hasChanges,
414
+ wasSuccessful,
415
+ recentlySuccessful,
416
+ submit,
417
+ reset,
418
+ clearErrors,
419
+ clearError,
420
+ post: (url) => submit(url, "POST"),
421
+ put: (url) => submit(url, "PUT"),
422
+ patch: (url) => submit(url, "PATCH"),
423
+ delete: (url) => submit(url, "DELETE")
424
+ });
425
+ }
426
+ //#endregion
427
+ //#region src/runtime/action.ts
428
+ const client = createActionClient();
429
+ const action = client.action;
430
+ const setActionRouter = client.setActionRouter;
431
+ //#endregion
2
432
  export { Form, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
package/dist/plugin.mjs CHANGED
@@ -51,30 +51,13 @@ const COMPONENT_EXTENSIONS = [
51
51
  ".ts",
52
52
  ".js"
53
53
  ];
54
- function extractCss(filePath) {
55
- const code = readFileSync(filePath, "utf-8");
56
- const parts = [];
57
- const jsRe = /^\s*import\b[^"']*["']([^"']+\.(?:css|scss|sass|less|styl|stylus|pcss|postcss))["']/gm;
58
- let match;
59
- while ((match = jsRe.exec(code)) !== null) parts.push(`@import ${JSON.stringify(match[1])};`);
60
- const styleBlockRe = /<style\b([^>]*)>([\s\S]*?)<\/style>/g;
61
- while ((match = styleBlockRe.exec(code)) !== null) {
62
- const attrs = match[1];
63
- if (/\bscoped\b/.test(attrs)) continue;
64
- const content = match[2].trim();
65
- if (content) parts.push(content);
66
- }
67
- return parts.join("\n");
68
- }
69
54
  function islandsPlugin(pagesDir, options) {
70
55
  let pageScan = null;
71
56
  const root = options?.root ?? dirname(pagesDir);
72
57
  const islandManifest = /* @__PURE__ */ new Map();
73
58
  const islandCssFiles = /* @__PURE__ */ new Set();
74
59
  let isDev = false;
75
- let viteRoot = "";
76
60
  let resolvedConfig = null;
77
- /** Resolve an island import and record it in the client manifest. */
78
61
  function registerIsland(spec, importerDir, hydrate) {
79
62
  const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
80
63
  if (!absPath) return {
@@ -97,7 +80,6 @@ function islandsPlugin(pagesDir, options) {
97
80
  enforce: "pre",
98
81
  configResolved(config) {
99
82
  resolvedConfig = config;
100
- viteRoot = config.root;
101
83
  isDev = config.command === "serve";
102
84
  },
103
85
  configureServer(server) {
@@ -138,21 +120,13 @@ function islandsPlugin(pagesDir, options) {
138
120
  }
139
121
  },
140
122
  resolveId: {
141
- filter: { id: /^virtual:void-islands-(renderer|client)$|\.vue\.css$/ },
123
+ filter: { id: /^virtual:void-islands-(renderer|client)$/ },
142
124
  handler(source) {
143
125
  if (source === ISLANDS_RENDERER_ID || source === ISLANDS_CLIENT_ENTRY_ID) return "\0" + source;
144
- if (source.endsWith(".css")) {
145
- const srcPath = normalizePath(source.slice(0, -4));
146
- if (islandCssFiles.has(srcPath)) return srcPath + ".css";
147
- if (srcPath.startsWith("/")) {
148
- const absPath = normalizePath(resolve(viteRoot, srcPath.slice(1)));
149
- if (islandCssFiles.has(absPath)) return absPath + ".css";
150
- }
151
- }
152
126
  }
153
127
  },
154
128
  load: {
155
- filter: { id: /^\0virtual:void-islands-(renderer|client)$|\.vue\.css$/ },
129
+ filter: { id: /^\0virtual:void-islands-(renderer|client)$/ },
156
130
  handler(id) {
157
131
  if (id === "\0" + ISLANDS_RENDERER_ID) {
158
132
  if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
@@ -165,10 +139,6 @@ function islandsPlugin(pagesDir, options) {
165
139
  if (!pageScan) return "";
166
140
  return generateIslandClientEntry(islandManifest, islandCssFiles, isDev);
167
141
  }
168
- if (id.endsWith(".css") && !id.startsWith("\0")) {
169
- const srcPath = id.slice(0, -4);
170
- if (islandCssFiles.has(srcPath)) return extractCss(srcPath);
171
- }
172
142
  }
173
143
  },
174
144
  transform: {
@@ -248,7 +218,6 @@ export async function renderIslandPage(pageObj, assetTags) {
248
218
  const app = createSSRApp({
249
219
  setup() {
250
220
  provide("__void_shared", reactive(pageObj.shared || {}));
251
- provide("__void_errors", reactive(pageObj.errors || {}));
252
221
  provide("__void_params", reactive(pageObj.params || {}));
253
222
  if (__frontmatter) provide("__void_frontmatter", __frontmatter);
254
223
  // No router provided — islands have no Void Router
@@ -499,7 +468,6 @@ export async function renderPage(pageObj, assetTags) {
499
468
  const app = createSSRApp({
500
469
  setup() {
501
470
  provide("__void_shared", reactive(pageObj.shared || {}));
502
- provide("__void_errors", reactive(pageObj.errors || {}));
503
471
  provide("__void_params", reactive(pageObj.params || {}));
504
472
  provide("__void_router", createSsrRouter(pageObj.url || "/", pageObj.params || {}));
505
473
  provide("__void_navigation", idleNavigationState());
@@ -544,7 +512,6 @@ import { parseCacheFor } from "void/pages-prefetch";
544
512
  ${generateComponentManifest(scan, pagesDir)}
545
513
 
546
514
  const shared = reactive({});
547
- const errors = reactive({});
548
515
  const params = reactive({});
549
516
  const currentComponent = shallowRef(null);
550
517
  const currentLayouts = shallowRef([]);
@@ -574,7 +541,6 @@ const { router, initialPageData, initialComponent, initialLayouts, start, commit
574
541
  applyUpdate(pageData, comp, layouts, options) {
575
542
  if (!options?.preserveState) currentPageKey.value++;
576
543
  replaceRecord(shared, pageData.shared || {});
577
- replaceRecord(errors, pageData.errors || {});
578
544
  replaceRecord(params, pageData.params || {});
579
545
  currentProps.value = pageData.props;
580
546
  currentLayouts.value = layouts;
@@ -603,7 +569,6 @@ const reactiveRouter = createRouterFacade(router, () => routeUrl.value, () => pa
603
569
  setActionRouter(reactiveRouter);
604
570
 
605
571
  Object.assign(shared, initialPageData.shared || {});
606
- replaceRecord(errors, initialPageData.errors || {});
607
572
  replaceRecord(params, initialPageData.params || {});
608
573
  currentProps.value = initialPageData.props;
609
574
  currentComponent.value = initialComponent;
@@ -615,7 +580,6 @@ const createVoidApp = appEl.getAttribute("data-void-ssr") === "false" ? createAp
615
580
  const app = createVoidApp({
616
581
  setup() {
617
582
  provide("__void_shared", shared);
618
- provide("__void_errors", errors);
619
583
  provide("__void_params", params);
620
584
  provide("__void_router", reactiveRouter);
621
585
  provide("__void_navigation", navigation);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/vue",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Vue pages, server rendering, forms, and navigation for Void.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -41,12 +41,12 @@
41
41
  "pathslash": "^0.1.0",
42
42
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
43
43
  "vite-plus": "1.1.0",
44
- "void": "0.26.1",
44
+ "void": "0.28.0",
45
45
  "vue": "^3.5.43"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "vite": "^8.0.0",
49
- "void": "0.26.1",
49
+ "void": "0.28.0",
50
50
  "vue": "^3.5.0"
51
51
  },
52
52
  "scripts": {
@@ -1,72 +0,0 @@
1
- import { ActionClient, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
2
- import { Component } from "vue";
3
- import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
4
- import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
5
- //#region src/runtime/form.d.ts
6
- declare const Form: Component;
7
- //#endregion
8
- //#region src/runtime/link.d.ts
9
- declare const Link: Component;
10
- //#endregion
11
- //#region src/runtime/use-router.d.ts
12
- declare function useRouter(): VoidRouter;
13
- //#endregion
14
- //#region src/runtime/use-params.d.ts
15
- declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
16
- //#endregion
17
- //#region src/runtime/use-shared.d.ts
18
- declare function useShared<T = CloudContextVariables["shared"]>(): T;
19
- //#endregion
20
- //#region src/runtime/use-form.d.ts
21
- type FormSubmitOptions = Omit<VisitOptions, "method" | "data"> & {
22
- data?: Record<string, unknown>;
23
- };
24
- declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
25
- data: ResolveActionBody<U>;
26
- errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
27
- error: VoidActionError | null;
28
- pending: boolean;
29
- hasChanges: boolean;
30
- wasSuccessful: boolean;
31
- recentlySuccessful: boolean;
32
- post(opts?: FormSubmitOptions): Promise<void>;
33
- put(opts?: FormSubmitOptions): Promise<void>;
34
- patch(opts?: FormSubmitOptions): Promise<void>;
35
- delete(opts?: FormSubmitOptions): Promise<void>;
36
- reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
37
- clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
38
- clearError(): void;
39
- };
40
- //#endregion
41
- //#region src/runtime/use-navigation.d.ts
42
- declare function useNavigation(): NavigationState;
43
- //#endregion
44
- //#region src/runtime/use-island-form.d.ts
45
- type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
46
- data: T;
47
- errors: Record<string, string>;
48
- error: VoidActionError | null;
49
- pending: boolean;
50
- hasChanges: boolean;
51
- wasSuccessful: boolean;
52
- recentlySuccessful: boolean;
53
- submit(url: string, method: string): Promise<void>;
54
- post(url: string): Promise<void>;
55
- put(url: string): Promise<void>;
56
- patch(url: string): Promise<void>;
57
- delete(url: string): Promise<void>;
58
- reset(...fields: ReadonlyArray<keyof T & string>): void;
59
- clearErrors(...fields: ReadonlyArray<string>): void;
60
- clearError(): void;
61
- }>;
62
- /**
63
- * Form composable for island pages. Uses fetch + page reload instead of the
64
- * Void Router (which is not available in island mode).
65
- */
66
- declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
67
- //#endregion
68
- //#region src/runtime/action.d.ts
69
- declare const action: ActionClient["action"];
70
- declare const setActionRouter: ActionClient["setActionRouter"];
71
- //#endregion
72
- export { useIslandForm as a, useShared as c, Link as d, Form as f, setActionRouter as i, useParams as l, InferProps as n, useNavigation as o, action as r, useForm as s, Deferred as t, useRouter as u };
@@ -1,2 +0,0 @@
1
- import { a as useIslandForm, c as useShared, d as Link, f as Form, i as setActionRouter, l as useParams, n as InferProps, o as useNavigation, r as action, s as useForm, t as Deferred, u as useRouter } from "../index-vAbysUNV.mjs";
2
- export { type Deferred, Form, type InferProps, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
@@ -1,2 +0,0 @@
1
- import { a as useForm, c as useRouter, i as useNavigation, l as Link, n as setActionRouter, o as useShared, r as useIslandForm, s as useParams, t as action, u as Form } from "../runtime-lPB1mgy1.mjs";
2
- export { Form, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
@@ -1,436 +0,0 @@
1
- import { VoidActionError, categorizeActionError, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
2
- import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, watch } from "vue";
3
- //#region src/runtime/form.ts
4
- const Form = defineComponent({
5
- name: "VoidForm",
6
- inheritAttrs: false,
7
- props: {
8
- form: {
9
- type: Object,
10
- required: true
11
- },
12
- method: {
13
- type: String,
14
- default: "post"
15
- },
16
- renderError: Function
17
- },
18
- setup(props, { attrs, slots }) {
19
- const now = ref(Date.now());
20
- let cancel = () => {};
21
- onMounted(() => watch(() => props.form.error, (error) => {
22
- cancel();
23
- cancel = scheduleFormRetry(error, () => {
24
- now.value = Date.now();
25
- });
26
- }, { immediate: true }));
27
- onUnmounted(() => cancel());
28
- return () => {
29
- const form = props.form;
30
- const message = formErrorMessage(form), retry = formRetryMessage(form.error);
31
- return h("form", {
32
- ...attrs,
33
- "aria-busy": form.pending,
34
- onSubmit: async (event) => {
35
- event.preventDefault();
36
- if (!formSubmissionBlocked(form, now.value)) await form[props.method]();
37
- }
38
- }, [message ? h("div", {
39
- role: "alert",
40
- "data-void-form-error": ""
41
- }, [form.error && props.renderError ? props.renderError(form.error) : message, retry ? h("p", retry) : null]) : null, slots.default?.()]);
42
- };
43
- }
44
- });
45
- //#endregion
46
- //#region src/runtime/link.ts
47
- function hasPrefetch(prefetch) {
48
- return prefetch !== false;
49
- }
50
- function callHandler(handler, event) {
51
- if (Array.isArray(handler)) {
52
- for (const fn of handler) if (typeof fn === "function") fn(event);
53
- } else if (typeof handler === "function") handler(event);
54
- }
55
- const Link = defineComponent({
56
- name: "Link",
57
- props: {
58
- href: {
59
- type: String,
60
- required: true
61
- },
62
- method: {
63
- type: String,
64
- default: "GET"
65
- },
66
- data: {
67
- type: Object,
68
- default: void 0
69
- },
70
- preserveScroll: {
71
- type: Boolean,
72
- default: false
73
- },
74
- preserveState: {
75
- type: Boolean,
76
- default: false
77
- },
78
- replace: {
79
- type: Boolean,
80
- default: false
81
- },
82
- reloadDocument: {
83
- type: Boolean,
84
- default: false
85
- },
86
- viewTransition: {
87
- type: Boolean,
88
- default: void 0
89
- },
90
- onNavigate: {
91
- type: Function,
92
- default: void 0
93
- },
94
- prefetch: {
95
- type: [
96
- Boolean,
97
- String,
98
- Array
99
- ],
100
- default: false
101
- },
102
- cacheFor: {
103
- type: [
104
- Number,
105
- String,
106
- Array
107
- ],
108
- default: void 0
109
- }
110
- },
111
- setup(props, { slots, attrs }) {
112
- const router = inject("__void_router", null);
113
- const elRef = ref(null);
114
- const normalizedMethod = computed(() => props.method.toUpperCase());
115
- const isGet = computed(() => normalizedMethod.value === "GET");
116
- const hrefWithData = computed(() => isGet.value ? mergeDataIntoHref(props.href, props.data) : props.href);
117
- const strategies = computed(() => {
118
- if (props.prefetch === false || props.reloadDocument) return [];
119
- if (props.prefetch === true) return ["hover"];
120
- if (typeof props.prefetch === "string") return [props.prefetch];
121
- return props.prefetch;
122
- });
123
- function doPrefetch() {
124
- if (!router?.prefetch) return;
125
- const current = window.location.pathname + window.location.search;
126
- if (hrefWithData.value === current) return;
127
- const options = {};
128
- if (props.cacheFor !== void 0) options.cacheFor = props.cacheFor;
129
- if (normalizedMethod.value) options.method = normalizedMethod.value;
130
- router.prefetch(hrefWithData.value, options);
131
- }
132
- function onClick(e) {
133
- callHandler(attrs.onClick, e);
134
- if (props.reloadDocument || !router || e.defaultPrevented || isModifiedEvent(e)) return;
135
- if (e.currentTarget.hasAttribute("download")) return;
136
- if (new URL(hrefWithData.value, window.location.origin).origin !== window.location.origin) return;
137
- let navigatePrevented = false;
138
- props.onNavigate?.({ preventDefault() {
139
- navigatePrevented = true;
140
- } });
141
- if (navigatePrevented) {
142
- e.preventDefault();
143
- return;
144
- }
145
- e.preventDefault();
146
- router.visit(hrefWithData.value, {
147
- method: normalizedMethod.value,
148
- data: isGet.value ? void 0 : props.data,
149
- preserveScroll: props.preserveScroll,
150
- preserveState: props.preserveState,
151
- replace: props.replace,
152
- viewTransition: props.viewTransition
153
- });
154
- }
155
- let hoverTimer = null;
156
- let observer = null;
157
- function onMouseEnter(e) {
158
- callHandler(attrs.onMouseenter ?? attrs.onMouseEnter, e);
159
- if (e.defaultPrevented) return;
160
- hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
161
- }
162
- function onMouseLeave(e) {
163
- callHandler(attrs.onMouseleave ?? attrs.onMouseLeave, e);
164
- if (hoverTimer) {
165
- clearTimeout(hoverTimer);
166
- hoverTimer = null;
167
- }
168
- }
169
- function onFocus(e) {
170
- callHandler(attrs.onFocus, e);
171
- if (!e.defaultPrevented) doPrefetch();
172
- }
173
- function onMouseDown(e) {
174
- callHandler(attrs.onMousedown ?? attrs.onMouseDown, e);
175
- if (!e.defaultPrevented) doPrefetch();
176
- }
177
- function onTouchStart(e) {
178
- callHandler(attrs.onTouchstart ?? attrs.onTouchStart, e);
179
- if (!e.defaultPrevented) doPrefetch();
180
- }
181
- onMounted(() => {
182
- if (strategies.value.includes("mount")) doPrefetch();
183
- if (strategies.value.includes("visible") && elRef.value) {
184
- observer = new IntersectionObserver((entries) => {
185
- if (entries.some((e) => e.isIntersecting)) {
186
- doPrefetch();
187
- observer?.disconnect();
188
- observer = null;
189
- }
190
- }, { rootMargin: "0px" });
191
- observer.observe(elRef.value);
192
- }
193
- });
194
- onUnmounted(() => {
195
- if (hoverTimer) clearTimeout(hoverTimer);
196
- if (observer) {
197
- observer.disconnect();
198
- observer = null;
199
- }
200
- });
201
- return () => {
202
- if (hasPrefetch(props.prefetch) && !isGet.value) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
203
- if (props.reloadDocument && !isGet.value) throw new Error("Link: reloadDocument only supports GET links.");
204
- const tag = isGet.value ? "a" : "button";
205
- const eventHandlers = { onClick };
206
- if (strategies.value.includes("hover")) {
207
- eventHandlers.onMouseenter = onMouseEnter;
208
- eventHandlers.onMouseleave = onMouseLeave;
209
- eventHandlers.onFocus = onFocus;
210
- eventHandlers.onTouchstart = onTouchStart;
211
- }
212
- if (strategies.value.includes("click")) {
213
- eventHandlers.onMousedown = onMouseDown;
214
- eventHandlers.onTouchstart = onTouchStart;
215
- }
216
- return h(tag, {
217
- ...attrs,
218
- ref: elRef,
219
- ...isGet.value ? { href: hrefWithData.value } : { type: "button" },
220
- style: [{ touchAction: "manipulation" }, attrs.style],
221
- ...eventHandlers
222
- }, slots.default?.());
223
- };
224
- }
225
- });
226
- //#endregion
227
- //#region src/runtime/use-router.ts
228
- function useRouter() {
229
- return inject("__void_router", null) ?? ssrProxy;
230
- }
231
- //#endregion
232
- //#region src/runtime/use-params.ts
233
- function useParams() {
234
- const params = inject("__void_params");
235
- if (!params) throw new Error("useParams(): must be used inside a Void page.");
236
- return params;
237
- }
238
- //#endregion
239
- //#region src/runtime/use-shared.ts
240
- function useShared() {
241
- const shared = inject("__void_shared");
242
- if (!shared) throw new Error("useShared(): must be used inside a Void page.");
243
- return shared;
244
- }
245
- //#endregion
246
- //#region src/runtime/use-form.ts
247
- function useForm(url, defaults, options) {
248
- const formDefaults = defaults;
249
- const router = inject("__void_router", null);
250
- const data = reactive({ ...formDefaults });
251
- const errors = reactive({});
252
- const error = ref(null);
253
- const pending = ref(false);
254
- const wasSuccessful = ref(false);
255
- const recentlySuccessful = ref(false);
256
- let defaults_ = { ...formDefaults };
257
- let successTimeout = null;
258
- const hasChanges = ref(false);
259
- watch(() => ({ ...data }), (val) => {
260
- hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
261
- }, { deep: true });
262
- function reset(...fields) {
263
- if (fields.length === 0) Object.assign(data, { ...defaults_ });
264
- else for (const field of fields) data[field] = defaults_[field];
265
- }
266
- function clearErrors(...fields) {
267
- if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
268
- else for (const field of fields) delete errors[field];
269
- }
270
- function clearError() {
271
- error.value = null;
272
- }
273
- async function submit(method, opts = {}) {
274
- pending.value = true;
275
- wasSuccessful.value = false;
276
- recentlySuccessful.value = false;
277
- if (successTimeout) clearTimeout(successTimeout);
278
- clearErrors();
279
- clearError();
280
- try {
281
- if (!router) throw new Error("useForm(): requires the Void Router.");
282
- const result = await submitAction(router, interpolateRouteParams(url, options?.params), {
283
- method,
284
- data: { ...data },
285
- ...opts
286
- });
287
- if (!result.ok) {
288
- const validationErrors = getValidationErrors(result.error.body);
289
- if (validationErrors) Object.assign(errors, validationErrors);
290
- else error.value = result.error;
291
- return;
292
- }
293
- wasSuccessful.value = true;
294
- recentlySuccessful.value = true;
295
- defaults_ = { ...data };
296
- successTimeout = setTimeout(() => {
297
- recentlySuccessful.value = false;
298
- }, 2e3);
299
- } catch (submitError) {
300
- if (isAbortError(submitError)) return;
301
- throw submitError;
302
- } finally {
303
- pending.value = false;
304
- }
305
- }
306
- return reactive({
307
- data,
308
- errors,
309
- error,
310
- pending,
311
- hasChanges,
312
- wasSuccessful,
313
- recentlySuccessful,
314
- reset,
315
- clearErrors,
316
- clearError,
317
- post: (opts) => submit("POST", opts),
318
- put: (opts) => submit("PUT", opts),
319
- patch: (opts) => submit("PATCH", opts),
320
- delete: (opts) => submit("DELETE", opts)
321
- });
322
- }
323
- //#endregion
324
- //#region src/runtime/use-navigation.ts
325
- function useNavigation() {
326
- return inject("__void_navigation", null) ?? idleNavigationState();
327
- }
328
- //#endregion
329
- //#region src/runtime/use-island-form.ts
330
- async function readActionErrorBody(response) {
331
- const contentType = response.headers.get("content-type") || "";
332
- try {
333
- if (contentType.includes("application/json")) return await response.json();
334
- return await response.text() || null;
335
- } catch {
336
- return null;
337
- }
338
- }
339
- /**
340
- * Form composable for island pages. Uses fetch + page reload instead of the
341
- * Void Router (which is not available in island mode).
342
- */
343
- function useIslandForm(defaults) {
344
- const data = reactive({ ...defaults });
345
- const errors = reactive({});
346
- const error = ref(null);
347
- const pending = ref(false);
348
- const wasSuccessful = ref(false);
349
- const recentlySuccessful = ref(false);
350
- let defaults_ = { ...defaults };
351
- let successTimeout = null;
352
- const hasChanges = ref(false);
353
- watch(() => ({ ...data }), (val) => {
354
- hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
355
- }, { deep: true });
356
- function reset(...fields) {
357
- if (fields.length === 0) Object.assign(data, { ...defaults_ });
358
- else for (const field of fields) data[field] = defaults_[field];
359
- }
360
- function clearErrors(...fields) {
361
- if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
362
- else for (const field of fields) delete errors[field];
363
- }
364
- function clearError() {
365
- error.value = null;
366
- }
367
- async function submit(url, method) {
368
- pending.value = true;
369
- wasSuccessful.value = false;
370
- recentlySuccessful.value = false;
371
- if (successTimeout) clearTimeout(successTimeout);
372
- clearErrors();
373
- clearError();
374
- try {
375
- const response = await fetch(url, {
376
- method,
377
- headers: { "Content-Type": "application/json" },
378
- body: JSON.stringify(data)
379
- });
380
- if (response.redirected) {
381
- window.location.href = response.url;
382
- return;
383
- }
384
- if (response.ok) {
385
- wasSuccessful.value = true;
386
- recentlySuccessful.value = true;
387
- defaults_ = { ...data };
388
- successTimeout = setTimeout(() => {
389
- recentlySuccessful.value = false;
390
- }, 2e3);
391
- window.location.reload();
392
- } else {
393
- const body = await readActionErrorBody(response);
394
- const actionError = new VoidActionError({
395
- body,
396
- status: response.status,
397
- statusText: response.statusText,
398
- url
399
- });
400
- const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
401
- if (validationErrors) {
402
- Object.assign(errors, validationErrors);
403
- return;
404
- }
405
- if (categorizeActionError(response.status) === "boundary") throw actionError;
406
- error.value = actionError;
407
- }
408
- } finally {
409
- pending.value = false;
410
- }
411
- }
412
- return reactive({
413
- data,
414
- errors,
415
- error,
416
- pending,
417
- hasChanges,
418
- wasSuccessful,
419
- recentlySuccessful,
420
- submit,
421
- reset,
422
- clearErrors,
423
- clearError,
424
- post: (url) => submit(url, "POST"),
425
- put: (url) => submit(url, "PUT"),
426
- patch: (url) => submit(url, "PATCH"),
427
- delete: (url) => submit(url, "DELETE")
428
- });
429
- }
430
- //#endregion
431
- //#region src/runtime/action.ts
432
- const client = createActionClient();
433
- const action = client.action;
434
- const setActionRouter = client.setActionRouter;
435
- //#endregion
436
- export { useForm as a, useRouter as c, useNavigation as i, Link as l, setActionRouter as n, useShared as o, useIslandForm as r, useParams as s, action as t, Form as u };