@void/vue 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +31 -46
- package/dist/plugin.mjs +47 -88
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionClient, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
|
|
1
|
+
import { ActionClient, FormResetOptions, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
|
|
2
2
|
import { Component } from "vue";
|
|
3
3
|
import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
|
|
4
4
|
import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
|
|
@@ -33,7 +33,9 @@ export declare function useForm<U extends ActionUrl & string>(url: U, defaults:
|
|
|
33
33
|
put(opts?: FormSubmitOptions): Promise<void>;
|
|
34
34
|
patch(opts?: FormSubmitOptions): Promise<void>;
|
|
35
35
|
delete(opts?: FormSubmitOptions): Promise<void>;
|
|
36
|
+
reset(options: FormResetOptions, ...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
36
37
|
reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
38
|
+
setError(field: keyof ResolveActionBody<U> & string, message: string): void;
|
|
37
39
|
clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
38
40
|
clearError(): void;
|
|
39
41
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
|
1
|
+
import { cloneFormValue, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, parseFormResetArgs, scheduleFormRetry, ssrProxy, submitAction, submitIslandAction } from "void/pages-client";
|
|
2
|
+
import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from "vue";
|
|
3
3
|
//#region src/runtime/form.ts
|
|
4
4
|
const Form = defineComponent({
|
|
5
5
|
name: "VoidForm",
|
|
@@ -241,28 +241,36 @@ function useShared() {
|
|
|
241
241
|
//#endregion
|
|
242
242
|
//#region src/runtime/use-form.ts
|
|
243
243
|
function useForm(url, defaults, options) {
|
|
244
|
-
const
|
|
244
|
+
const initialDefaults = cloneFormValue(defaults);
|
|
245
245
|
const router = inject("__void_router", null);
|
|
246
|
-
const data = reactive(
|
|
246
|
+
const data = reactive(cloneFormValue(initialDefaults));
|
|
247
247
|
const errors = reactive({});
|
|
248
248
|
const error = ref(null);
|
|
249
249
|
const pending = ref(false);
|
|
250
250
|
const wasSuccessful = ref(false);
|
|
251
251
|
const recentlySuccessful = ref(false);
|
|
252
|
-
|
|
252
|
+
const defaults_ = shallowRef(initialDefaults);
|
|
253
253
|
let successTimeout = null;
|
|
254
|
-
|
|
255
|
-
watch(() => (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
let dataSnapshot = cloneFormValue(initialDefaults);
|
|
255
|
+
watch(() => cloneFormValue(data), (val) => {
|
|
256
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(dataSnapshot), ...Object.keys(val)])) if (!isEqualFormValue(val[key], dataSnapshot[key])) delete errors[key];
|
|
257
|
+
dataSnapshot = val;
|
|
258
|
+
}, { flush: "sync" });
|
|
259
|
+
const hasChanges = computed(() => Object.keys(defaults_.value).some((key) => !isEqualFormValue(data[key], defaults_.value[key])));
|
|
260
|
+
function reset(...args) {
|
|
261
|
+
const { initial, fields } = parseFormResetArgs(args);
|
|
262
|
+
const source = initial ? initialDefaults : defaults_.value;
|
|
263
|
+
if (fields.length === 0) Object.assign(data, cloneFormValue(source));
|
|
264
|
+
else for (const field of fields) data[field] = cloneFormValue(source[field]);
|
|
265
|
+
dataSnapshot = cloneFormValue(data);
|
|
261
266
|
}
|
|
262
267
|
function clearErrors(...fields) {
|
|
263
268
|
if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
|
|
264
269
|
else for (const field of fields) delete errors[field];
|
|
265
270
|
}
|
|
271
|
+
function setError(field, message) {
|
|
272
|
+
errors[field] = message;
|
|
273
|
+
}
|
|
266
274
|
function clearError() {
|
|
267
275
|
error.value = null;
|
|
268
276
|
}
|
|
@@ -288,7 +296,10 @@ function useForm(url, defaults, options) {
|
|
|
288
296
|
}
|
|
289
297
|
wasSuccessful.value = true;
|
|
290
298
|
recentlySuccessful.value = true;
|
|
291
|
-
|
|
299
|
+
if (options?.resetOnSuccess) {
|
|
300
|
+
Object.assign(data, cloneFormValue(initialDefaults));
|
|
301
|
+
defaults_.value = cloneFormValue(initialDefaults);
|
|
302
|
+
} else defaults_.value = cloneFormValue(data);
|
|
292
303
|
successTimeout = setTimeout(() => {
|
|
293
304
|
recentlySuccessful.value = false;
|
|
294
305
|
}, 2e3);
|
|
@@ -308,6 +319,7 @@ function useForm(url, defaults, options) {
|
|
|
308
319
|
wasSuccessful,
|
|
309
320
|
recentlySuccessful,
|
|
310
321
|
reset,
|
|
322
|
+
setError,
|
|
311
323
|
clearErrors,
|
|
312
324
|
clearError,
|
|
313
325
|
post: (opts) => submit("POST", opts),
|
|
@@ -323,15 +335,6 @@ function useNavigation() {
|
|
|
323
335
|
}
|
|
324
336
|
//#endregion
|
|
325
337
|
//#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
338
|
/**
|
|
336
339
|
* Form composable for island pages. Uses fetch + page reload instead of the
|
|
337
340
|
* Void Router (which is not available in island mode).
|
|
@@ -368,16 +371,12 @@ function useIslandForm(defaults) {
|
|
|
368
371
|
clearErrors();
|
|
369
372
|
clearError();
|
|
370
373
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
body: JSON.stringify(data)
|
|
375
|
-
});
|
|
376
|
-
if (response.redirected) {
|
|
377
|
-
window.location.href = response.url;
|
|
374
|
+
const result = await submitIslandAction(url, method, data);
|
|
375
|
+
if (result.kind === "redirect") {
|
|
376
|
+
window.location.href = result.url;
|
|
378
377
|
return;
|
|
379
378
|
}
|
|
380
|
-
if (
|
|
379
|
+
if (result.kind === "success") {
|
|
381
380
|
wasSuccessful.value = true;
|
|
382
381
|
recentlySuccessful.value = true;
|
|
383
382
|
defaults_ = { ...data };
|
|
@@ -385,22 +384,8 @@ function useIslandForm(defaults) {
|
|
|
385
384
|
recentlySuccessful.value = false;
|
|
386
385
|
}, 2e3);
|
|
387
386
|
window.location.reload();
|
|
388
|
-
} else
|
|
389
|
-
|
|
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
|
-
}
|
|
387
|
+
} else if (result.kind === "validation") Object.assign(errors, result.errors);
|
|
388
|
+
else error.value = result.error;
|
|
404
389
|
} finally {
|
|
405
390
|
pending.value = false;
|
|
406
391
|
}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import vue from "@vitejs/plugin-vue";
|
|
2
|
-
import { generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir,
|
|
2
|
+
import { createPagesPlugin, generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, scanPages } from "void/pages";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import nodePath from "node:path";
|
|
5
5
|
import { normalizePath } from "vite";
|
|
@@ -89,7 +89,10 @@ function islandsPlugin(pagesDir, options) {
|
|
|
89
89
|
},
|
|
90
90
|
async buildStart() {
|
|
91
91
|
if (existsSync(pagesDir)) {
|
|
92
|
-
pageScan = await scanPages(root, {
|
|
92
|
+
pageScan = await scanPages(root, {
|
|
93
|
+
pagesDir,
|
|
94
|
+
environment: isDev ? "dev" : "prod"
|
|
95
|
+
});
|
|
93
96
|
const filesToScan = [
|
|
94
97
|
...pageScan.pages.map((page) => ({
|
|
95
98
|
filePath: page.componentPath,
|
|
@@ -310,8 +313,36 @@ initIslands(islands, hydrateIsland);
|
|
|
310
313
|
}
|
|
311
314
|
//#endregion
|
|
312
315
|
//#region src/plugin-pages.ts
|
|
313
|
-
const RENDERER_ID = "virtual:void-pages-renderer";
|
|
314
316
|
const CLIENT_ENTRY_ID = "virtual:void-pages-client";
|
|
317
|
+
/** Runtime helper injected into generated SSR and client page bundles. */
|
|
318
|
+
const PATCH_PROPS_RUNTIME = `
|
|
319
|
+
function hasRuntimePropsDeclaration(comp) {
|
|
320
|
+
const declared = comp.props;
|
|
321
|
+
if (declared == null) return false;
|
|
322
|
+
if (Array.isArray(declared)) return declared.length > 0;
|
|
323
|
+
if (typeof declared === "object") return Object.keys(declared).length > 0;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function patchProps(comp, props) {
|
|
328
|
+
if (comp.__voidPatched) return;
|
|
329
|
+
const keys = Object.keys(props);
|
|
330
|
+
if (hasRuntimePropsDeclaration(comp)) {
|
|
331
|
+
const declared = comp.props;
|
|
332
|
+
// defineModel() can declare runtime props even after typed loader props are stripped.
|
|
333
|
+
const missing = keys.filter((key) =>
|
|
334
|
+
Array.isArray(declared) ? !declared.includes(key) : !Object.hasOwn(declared, key)
|
|
335
|
+
);
|
|
336
|
+
if (missing.length === 0) return;
|
|
337
|
+
comp.props = Array.isArray(declared)
|
|
338
|
+
? [...declared, ...missing]
|
|
339
|
+
: { ...Object.fromEntries(missing.map((key) => [key, null])), ...declared };
|
|
340
|
+
} else {
|
|
341
|
+
comp.props = keys;
|
|
342
|
+
}
|
|
343
|
+
comp.__voidPatched = true;
|
|
344
|
+
}
|
|
345
|
+
`;
|
|
315
346
|
function isVuePageFile(id, pagesDir) {
|
|
316
347
|
if (!id.startsWith(pagesDir + "/")) return false;
|
|
317
348
|
const rel = id.slice(pagesDir.length + 1);
|
|
@@ -357,34 +388,14 @@ function hasNonIslandPages(dir) {
|
|
|
357
388
|
return false;
|
|
358
389
|
}
|
|
359
390
|
function pagesPlugin(pagesDir, options) {
|
|
360
|
-
let pageScan = null;
|
|
361
|
-
let isDev = false;
|
|
362
|
-
let viteConfig;
|
|
363
|
-
const root = options?.root ?? dirname(pagesDir);
|
|
364
391
|
const normalizedPagesDir = normalizePath(pagesDir);
|
|
365
|
-
let pendingRescan = null;
|
|
366
|
-
const rescanPages = async () => {
|
|
367
|
-
pageScan = existsSync(pagesDir) ? await scanPages(root, { pagesDir }) : null;
|
|
368
|
-
};
|
|
369
|
-
const runRescan = () => {
|
|
370
|
-
pendingRescan ??= rescanPages().finally(() => {
|
|
371
|
-
pendingRescan = null;
|
|
372
|
-
});
|
|
373
|
-
return pendingRescan;
|
|
374
|
-
};
|
|
375
|
-
const invalidatePageModules = (server) => {
|
|
376
|
-
for (const env of Object.values(server.environments)) for (const id of ["\0" + RENDERER_ID, "\0" + CLIENT_ENTRY_ID]) {
|
|
377
|
-
const mod = env.moduleGraph.getModuleById(id);
|
|
378
|
-
if (mod) env.moduleGraph.invalidateModule(mod);
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
392
|
return [{
|
|
382
393
|
name: "void-vue:pages-transform",
|
|
383
394
|
enforce: "pre",
|
|
384
395
|
transform: {
|
|
385
396
|
filter: { id: /\.vue$/ },
|
|
386
397
|
handler(code, id) {
|
|
387
|
-
if (!isVuePageFile(id,
|
|
398
|
+
if (!isVuePageFile(normalizePath(id), normalizedPagesDir)) return;
|
|
388
399
|
const transformed = stripDefinePropsTypeArgument(code);
|
|
389
400
|
if (!transformed) return;
|
|
390
401
|
return {
|
|
@@ -396,7 +407,7 @@ function pagesPlugin(pagesDir, options) {
|
|
|
396
407
|
handleHotUpdate: {
|
|
397
408
|
order: "pre",
|
|
398
409
|
handler(ctx) {
|
|
399
|
-
if (!isVuePageFile(ctx.file,
|
|
410
|
+
if (!isVuePageFile(normalizePath(ctx.file), normalizedPagesDir)) return;
|
|
400
411
|
const read = ctx.read;
|
|
401
412
|
ctx.read = async () => {
|
|
402
413
|
const code = await read();
|
|
@@ -404,68 +415,24 @@ function pagesPlugin(pagesDir, options) {
|
|
|
404
415
|
};
|
|
405
416
|
}
|
|
406
417
|
}
|
|
407
|
-
}, {
|
|
418
|
+
}, createPagesPlugin(pagesDir, {
|
|
408
419
|
name: "void-vue:pages",
|
|
409
|
-
|
|
410
|
-
|
|
420
|
+
root: options?.root,
|
|
421
|
+
generateRenderer(scan) {
|
|
422
|
+
return generateRendererModule(scan, pagesDir);
|
|
411
423
|
},
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
handler(id) {
|
|
415
|
-
return "\0" + id;
|
|
416
|
-
}
|
|
424
|
+
generateClient(scan, staticPageData) {
|
|
425
|
+
return generateClientEntry(scan, pagesDir, options?.viewTransitions, options?.prefetch, staticPageData);
|
|
417
426
|
},
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
async handler(id) {
|
|
421
|
-
if (isDev && pendingRescan) await pendingRescan;
|
|
422
|
-
if (!pageScan && existsSync(pagesDir)) await runRescan();
|
|
423
|
-
if (id === "\0" + RENDERER_ID) {
|
|
424
|
-
if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
|
|
425
|
-
return generateRendererModule(pageScan, pagesDir);
|
|
426
|
-
}
|
|
427
|
-
if (id === "\0" + CLIENT_ENTRY_ID) {
|
|
428
|
-
if (!pageScan) return "";
|
|
429
|
-
return generateClientEntry(pageScan, pagesDir, options?.viewTransitions, options?.prefetch, resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
configureServer(server) {
|
|
434
|
-
const handlePagesChange = async (path, event) => {
|
|
435
|
-
const normalizedPath = normalizePath(path);
|
|
436
|
-
if (normalizedPath !== normalizedPagesDir && !normalizedPath.startsWith(normalizedPagesDir + "/")) return;
|
|
437
|
-
if (event === "change" && !normalizedPath.endsWith(".md")) return;
|
|
438
|
-
try {
|
|
439
|
-
await runRescan();
|
|
440
|
-
} catch (error) {
|
|
441
|
-
server.config.logger.error(error instanceof Error ? error.message : String(error), {
|
|
442
|
-
error: error instanceof Error ? error : void 0,
|
|
443
|
-
timestamp: true
|
|
444
|
-
});
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
invalidatePageModules(server);
|
|
448
|
-
};
|
|
449
|
-
server.watcher.on("add", (path) => handlePagesChange(path, "add"));
|
|
450
|
-
server.watcher.on("change", (path) => handlePagesChange(path, "change"));
|
|
451
|
-
server.watcher.on("unlink", (path) => handlePagesChange(path, "unlink"));
|
|
452
|
-
},
|
|
453
|
-
configResolved(config) {
|
|
454
|
-
viteConfig = config;
|
|
455
|
-
},
|
|
456
|
-
config(_, env) {
|
|
457
|
-
if (!existsSync(pagesDir)) return;
|
|
458
|
-
if (env.command === "serve") {
|
|
459
|
-
isDev = true;
|
|
460
|
-
return { optimizeDeps: { include: ["vue"] } };
|
|
461
|
-
}
|
|
427
|
+
config({ isDev }) {
|
|
428
|
+
if (isDev) return { optimizeDeps: { include: ["vue"] } };
|
|
462
429
|
if (!hasNonIslandPages(pagesDir)) return;
|
|
463
430
|
return { environments: { client: { build: {
|
|
464
431
|
manifest: true,
|
|
465
432
|
rollupOptions: { input: { "pages-client": CLIENT_ENTRY_ID } }
|
|
466
433
|
} } } };
|
|
467
434
|
}
|
|
468
|
-
}];
|
|
435
|
+
})];
|
|
469
436
|
}
|
|
470
437
|
function generateRendererModule(scan, pagesDir) {
|
|
471
438
|
return `
|
|
@@ -477,11 +444,7 @@ import { serializePageData } from "void/pages-server";
|
|
|
477
444
|
|
|
478
445
|
${generateComponentManifest(scan, pagesDir)}
|
|
479
446
|
|
|
480
|
-
|
|
481
|
-
if (comp.__voidPatched) return;
|
|
482
|
-
comp.props = Object.keys(props);
|
|
483
|
-
comp.__voidPatched = true;
|
|
484
|
-
}
|
|
447
|
+
${PATCH_PROPS_RUNTIME}
|
|
485
448
|
|
|
486
449
|
export async function renderPage(pageObj, assetTags) {
|
|
487
450
|
const PageComponent = (await components[pageObj.component]()).default;
|
|
@@ -545,11 +508,7 @@ const currentPageKey = ref(0);
|
|
|
545
508
|
const routeUrl = ref(window.location.pathname + window.location.search + window.location.hash);
|
|
546
509
|
const navigation = reactive(idleNavigationState());
|
|
547
510
|
|
|
548
|
-
|
|
549
|
-
if (comp.__voidPatched) return;
|
|
550
|
-
comp.props = Object.keys(props);
|
|
551
|
-
comp.__voidPatched = true;
|
|
552
|
-
}
|
|
511
|
+
${PATCH_PROPS_RUNTIME}
|
|
553
512
|
|
|
554
513
|
function replaceRecord(target, source) {
|
|
555
514
|
for (const key of Object.keys(target)) delete target[key];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/vue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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.3.0",
|
|
43
43
|
"vite-plus": "1.3.0",
|
|
44
|
-
"void": "1.
|
|
44
|
+
"void": "1.4.0",
|
|
45
45
|
"vue": "^3.5.44"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"vite": "^8.0.0",
|
|
49
|
-
"void": "1.
|
|
49
|
+
"void": "1.4.0",
|
|
50
50
|
"vue": "^3.5.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|