@void/svelte 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/plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
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 { preprocess } from "svelte/compiler";
|
|
@@ -95,7 +95,10 @@ function islandsPlugin(pagesDir, options) {
|
|
|
95
95
|
},
|
|
96
96
|
async buildStart() {
|
|
97
97
|
if (existsSync(pagesDir)) {
|
|
98
|
-
pageScan = await scanPages(root, {
|
|
98
|
+
pageScan = await scanPages(root, {
|
|
99
|
+
pagesDir,
|
|
100
|
+
environment: isDev ? "dev" : "prod"
|
|
101
|
+
});
|
|
99
102
|
const filesToScan = [
|
|
100
103
|
...pageScan.pages.map((page) => ({
|
|
101
104
|
filePath: page.componentPath,
|
|
@@ -326,7 +329,6 @@ initIslands(islands, hydrateIsland);
|
|
|
326
329
|
}
|
|
327
330
|
//#endregion
|
|
328
331
|
//#region src/plugin-pages.ts
|
|
329
|
-
const RENDERER_ID = "virtual:void-pages-renderer";
|
|
330
332
|
const CLIENT_ENTRY_ID = "virtual:void-pages-client";
|
|
331
333
|
const SVELTE_OPTIMIZE_DEPS = {
|
|
332
334
|
exclude: ["@void/svelte"],
|
|
@@ -353,92 +355,27 @@ function hasNonIslandPages(dir) {
|
|
|
353
355
|
return false;
|
|
354
356
|
}
|
|
355
357
|
function pagesPlugin(pagesDir, options) {
|
|
356
|
-
|
|
357
|
-
let isDev = false;
|
|
358
|
-
let viteConfig;
|
|
359
|
-
const root = options?.root ?? dirname(pagesDir);
|
|
360
|
-
const normalizedPagesDir = normalizePath(pagesDir);
|
|
361
|
-
let pendingRescan = null;
|
|
362
|
-
const rescanPages = async () => {
|
|
363
|
-
pageScan = existsSync(pagesDir) ? await scanPages(root, { pagesDir }) : null;
|
|
364
|
-
};
|
|
365
|
-
const runRescan = () => {
|
|
366
|
-
pendingRescan ??= rescanPages().finally(() => {
|
|
367
|
-
pendingRescan = null;
|
|
368
|
-
});
|
|
369
|
-
return pendingRescan;
|
|
370
|
-
};
|
|
371
|
-
const invalidatePageModules = (server) => {
|
|
372
|
-
for (const env of Object.values(server.environments)) for (const id of ["\0" + RENDERER_ID, "\0" + CLIENT_ENTRY_ID]) {
|
|
373
|
-
const mod = env.moduleGraph.getModuleById(id);
|
|
374
|
-
if (mod) env.moduleGraph.invalidateModule(mod);
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
return {
|
|
358
|
+
return createPagesPlugin(pagesDir, {
|
|
378
359
|
name: "void-svelte:pages",
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
resolveId: {
|
|
383
|
-
filter: { id: /^virtual:void-pages-(renderer|client)$/ },
|
|
384
|
-
handler(id) {
|
|
385
|
-
return "\0" + id;
|
|
386
|
-
}
|
|
360
|
+
root: options?.root,
|
|
361
|
+
generateRenderer(scan) {
|
|
362
|
+
return generateRendererModule(scan, pagesDir);
|
|
387
363
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
async handler(id) {
|
|
391
|
-
if (isDev && pendingRescan) await pendingRescan;
|
|
392
|
-
if (!pageScan && existsSync(pagesDir)) await runRescan();
|
|
393
|
-
if (id === "\0" + RENDERER_ID) {
|
|
394
|
-
if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
|
|
395
|
-
return generateRendererModule(pageScan, pagesDir);
|
|
396
|
-
}
|
|
397
|
-
if (id === "\0" + CLIENT_ENTRY_ID) {
|
|
398
|
-
if (!pageScan) return "";
|
|
399
|
-
return generateClientEntry(pageScan, pagesDir, options?.viewTransitions, options?.prefetch, resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }));
|
|
400
|
-
}
|
|
401
|
-
}
|
|
364
|
+
generateClient(scan, staticPageData) {
|
|
365
|
+
return generateClientEntry(scan, pagesDir, options?.viewTransitions, options?.prefetch, staticPageData);
|
|
402
366
|
},
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (event === "change" && !normalizedPath.endsWith(".md")) return;
|
|
408
|
-
try {
|
|
409
|
-
await runRescan();
|
|
410
|
-
} catch (error) {
|
|
411
|
-
server.config.logger.error(error instanceof Error ? error.message : String(error), {
|
|
412
|
-
error: error instanceof Error ? error : void 0,
|
|
413
|
-
timestamp: true
|
|
414
|
-
});
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
invalidatePageModules(server);
|
|
367
|
+
config({ isDev }) {
|
|
368
|
+
if (isDev) return {
|
|
369
|
+
optimizeDeps: SVELTE_OPTIMIZE_DEPS,
|
|
370
|
+
environments: { void_worker: { optimizeDeps: { exclude: ["@void/svelte"] } } }
|
|
418
371
|
};
|
|
419
|
-
server.watcher.on("add", (path) => handlePagesChange(path, "add"));
|
|
420
|
-
server.watcher.on("change", (path) => handlePagesChange(path, "change"));
|
|
421
|
-
server.watcher.on("unlink", (path) => handlePagesChange(path, "unlink"));
|
|
422
|
-
},
|
|
423
|
-
configResolved(config) {
|
|
424
|
-
viteConfig = config;
|
|
425
|
-
},
|
|
426
|
-
config(_, env) {
|
|
427
|
-
if (!existsSync(pagesDir)) return;
|
|
428
|
-
if (env.command === "serve") {
|
|
429
|
-
isDev = true;
|
|
430
|
-
return {
|
|
431
|
-
optimizeDeps: SVELTE_OPTIMIZE_DEPS,
|
|
432
|
-
environments: { void_worker: { optimizeDeps: { exclude: ["@void/svelte"] } } }
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
372
|
if (!hasNonIslandPages(pagesDir)) return;
|
|
436
373
|
return { build: {
|
|
437
374
|
manifest: true,
|
|
438
375
|
rollupOptions: { input: { "pages-client": CLIENT_ENTRY_ID } }
|
|
439
376
|
} };
|
|
440
377
|
}
|
|
441
|
-
};
|
|
378
|
+
});
|
|
442
379
|
}
|
|
443
380
|
function generateRendererModule(scan, pagesDir) {
|
|
444
381
|
return `
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/svelte",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Svelte pages, server rendering, forms, and navigation for Void.",
|
|
5
5
|
"homepage": "https://void.cloud",
|
|
6
6
|
"bugs": {
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"svelte": "^5.57.2",
|
|
44
44
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
|
|
45
45
|
"vite-plus": "1.3.0",
|
|
46
|
-
"void": "1.
|
|
46
|
+
"void": "1.4.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"svelte": "^5.0.0",
|
|
50
50
|
"vite": "^8.0.0",
|
|
51
|
-
"void": "1.
|
|
51
|
+
"void": "1.4.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "vp pack",
|
package/src/runtime/App.svelte
CHANGED
|
@@ -30,12 +30,13 @@
|
|
|
30
30
|
let navigation = $state(init.navigation || idleNavigationState());
|
|
31
31
|
const router = createRouterFacade(init.router, () => routeUrl, () => params);
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
// Pages and layouts keep the objects returned by useShared() and useParams()
|
|
34
|
+
// across navigations, so update them in place.
|
|
35
|
+
function replaceInPlace(target, next) {
|
|
36
|
+
for (const key of Object.keys(target)) {
|
|
37
|
+
delete target[key];
|
|
37
38
|
}
|
|
38
|
-
Object.assign(
|
|
39
|
+
Object.assign(target, next || {});
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Capitalized alias for dynamic component rendering
|
|
@@ -65,8 +66,8 @@
|
|
|
65
66
|
init.stores.page.subscribe((v) => { currentPage = v; }),
|
|
66
67
|
init.stores.props.subscribe((v) => { currentPageProps = v; }),
|
|
67
68
|
init.stores.layouts.subscribe((v) => { currentLayouts = v; }),
|
|
68
|
-
init.stores.shared.subscribe(
|
|
69
|
-
init.stores.params.subscribe((v) =>
|
|
69
|
+
init.stores.shared.subscribe((v) => replaceInPlace(shared, v)),
|
|
70
|
+
init.stores.params.subscribe((v) => replaceInPlace(params, v)),
|
|
70
71
|
init.stores.route.subscribe((v) => { routeUrl = v; }),
|
|
71
72
|
init.stores.navigation.subscribe((v) => { navigation = v; }),
|
|
72
73
|
];
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { getContext } from 'svelte';
|
|
1
|
+
import { getContext, untrack } from 'svelte';
|
|
2
2
|
import {
|
|
3
3
|
VoidActionError,
|
|
4
|
+
cloneFormValue,
|
|
4
5
|
getValidationErrors,
|
|
5
6
|
interpolateRouteParams,
|
|
6
7
|
isAbortError,
|
|
7
8
|
isEqualFormValue,
|
|
9
|
+
parseFormResetArgs,
|
|
8
10
|
submitAction,
|
|
11
|
+
type FormResetOptions,
|
|
9
12
|
type VisitOptions,
|
|
10
13
|
type VoidRouter,
|
|
11
14
|
} from 'void/pages-client';
|
|
@@ -26,7 +29,9 @@ type UntypedForm = {
|
|
|
26
29
|
put(opts?: FormSubmitOptions): Promise<void>;
|
|
27
30
|
patch(opts?: FormSubmitOptions): Promise<void>;
|
|
28
31
|
delete(opts?: FormSubmitOptions): Promise<void>;
|
|
32
|
+
reset(options: FormResetOptions, ...fields: ReadonlyArray<string>): void;
|
|
29
33
|
reset(...fields: ReadonlyArray<string>): void;
|
|
34
|
+
setError(field: string, message: string): void;
|
|
30
35
|
clearErrors(...fields: ReadonlyArray<string>): void;
|
|
31
36
|
clearError(): void;
|
|
32
37
|
};
|
|
@@ -48,7 +53,12 @@ export function useForm<U extends ActionUrl & string>(
|
|
|
48
53
|
put(opts?: FormSubmitOptions): Promise<void>;
|
|
49
54
|
patch(opts?: FormSubmitOptions): Promise<void>;
|
|
50
55
|
delete(opts?: FormSubmitOptions): Promise<void>;
|
|
56
|
+
reset(
|
|
57
|
+
options: FormResetOptions,
|
|
58
|
+
...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>
|
|
59
|
+
): void;
|
|
51
60
|
reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
61
|
+
setError(field: keyof ResolveActionBody<U> & string, message: string): void;
|
|
52
62
|
clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
53
63
|
clearError(): void;
|
|
54
64
|
};
|
|
@@ -56,32 +66,50 @@ export function useForm<U extends ActionUrl & string>(
|
|
|
56
66
|
export function useForm(
|
|
57
67
|
url: string,
|
|
58
68
|
defaults: Record<string, unknown>,
|
|
59
|
-
options?: { params?: Record<string, string
|
|
69
|
+
options?: { params?: Record<string, string>; resetOnSuccess?: boolean },
|
|
60
70
|
): UntypedForm {
|
|
61
71
|
const router = getContext<VoidRouter>('__void_router');
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
const initialDefaults = cloneFormValue(defaults);
|
|
74
|
+
let data = $state(cloneFormValue(initialDefaults));
|
|
64
75
|
let errors: Record<string, string> = $state({});
|
|
65
76
|
let error: VoidActionError | null = $state(null);
|
|
66
77
|
let pending = $state(false);
|
|
67
78
|
let wasSuccessful = $state(false);
|
|
68
79
|
let recentlySuccessful = $state(false);
|
|
69
80
|
|
|
70
|
-
let defaults_ =
|
|
81
|
+
let defaults_ = $state.raw(initialDefaults);
|
|
71
82
|
let successTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
let dataSnapshot = cloneFormValue(initialDefaults);
|
|
84
|
+
|
|
85
|
+
$effect(() => {
|
|
86
|
+
// Read nested values even after an earlier edit, without subscribing to errors.
|
|
87
|
+
const current = cloneFormValue(data);
|
|
88
|
+
untrack(() => {
|
|
89
|
+
for (const key of new Set([...Object.keys(dataSnapshot), ...Object.keys(current)])) {
|
|
90
|
+
if (!isEqualFormValue(current[key], dataSnapshot[key])) {
|
|
91
|
+
delete errors[key];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
dataSnapshot = current;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
72
97
|
|
|
73
98
|
const hasChanges = $derived(
|
|
74
99
|
Object.keys(defaults_).some((key) => !isEqualFormValue(data[key], defaults_[key])),
|
|
75
100
|
);
|
|
76
101
|
|
|
77
|
-
function reset(...
|
|
102
|
+
function reset(...args: ReadonlyArray<string | FormResetOptions>) {
|
|
103
|
+
const { initial, fields } = parseFormResetArgs(args);
|
|
104
|
+
const source = initial ? initialDefaults : defaults_;
|
|
78
105
|
if (fields.length === 0) {
|
|
79
|
-
data =
|
|
106
|
+
data = cloneFormValue(source);
|
|
80
107
|
} else {
|
|
81
108
|
for (const field of fields) {
|
|
82
|
-
data[field] =
|
|
109
|
+
data[field] = cloneFormValue(source[field]);
|
|
83
110
|
}
|
|
84
111
|
}
|
|
112
|
+
dataSnapshot = cloneFormValue(data);
|
|
85
113
|
}
|
|
86
114
|
|
|
87
115
|
function clearErrors(...fields: ReadonlyArray<string>) {
|
|
@@ -94,6 +122,12 @@ export function useForm(
|
|
|
94
122
|
}
|
|
95
123
|
}
|
|
96
124
|
|
|
125
|
+
function setError(field: string, message: string) {
|
|
126
|
+
// An input handler may validate its new value before the change effect runs.
|
|
127
|
+
dataSnapshot = { ...dataSnapshot, [field]: cloneFormValue(data[field]) };
|
|
128
|
+
errors = { ...errors, [field]: message };
|
|
129
|
+
}
|
|
130
|
+
|
|
97
131
|
function clearError() {
|
|
98
132
|
error = null;
|
|
99
133
|
}
|
|
@@ -120,7 +154,9 @@ export function useForm(
|
|
|
120
154
|
if (!result.ok) {
|
|
121
155
|
const validationErrors = getValidationErrors(result.error.body);
|
|
122
156
|
if (validationErrors) {
|
|
123
|
-
Object.
|
|
157
|
+
for (const [field, message] of Object.entries(validationErrors)) {
|
|
158
|
+
setError(field, message);
|
|
159
|
+
}
|
|
124
160
|
} else {
|
|
125
161
|
error = result.error;
|
|
126
162
|
}
|
|
@@ -129,7 +165,12 @@ export function useForm(
|
|
|
129
165
|
|
|
130
166
|
wasSuccessful = true;
|
|
131
167
|
recentlySuccessful = true;
|
|
132
|
-
|
|
168
|
+
if (options?.resetOnSuccess) {
|
|
169
|
+
data = cloneFormValue(initialDefaults);
|
|
170
|
+
defaults_ = cloneFormValue(initialDefaults);
|
|
171
|
+
} else {
|
|
172
|
+
defaults_ = cloneFormValue(data);
|
|
173
|
+
}
|
|
133
174
|
successTimeout = setTimeout(() => {
|
|
134
175
|
recentlySuccessful = false;
|
|
135
176
|
}, 2000);
|
|
@@ -166,6 +207,7 @@ export function useForm(
|
|
|
166
207
|
return recentlySuccessful;
|
|
167
208
|
},
|
|
168
209
|
reset,
|
|
210
|
+
setError,
|
|
169
211
|
clearErrors,
|
|
170
212
|
clearError,
|
|
171
213
|
post: (opts?: FormSubmitOptions) => submit('POST', opts),
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
VoidActionError,
|
|
3
|
-
categorizeActionError,
|
|
4
|
-
getValidationErrors,
|
|
5
|
-
isEqualFormValue,
|
|
6
|
-
} from 'void/pages-client';
|
|
1
|
+
import { isEqualFormValue, submitIslandAction, type VoidActionError } from 'void/pages-client';
|
|
7
2
|
|
|
8
3
|
type UntypedIslandForm = {
|
|
9
4
|
readonly data: Record<string, unknown>;
|
|
@@ -23,19 +18,6 @@ type UntypedIslandForm = {
|
|
|
23
18
|
clearError(): void;
|
|
24
19
|
};
|
|
25
20
|
|
|
26
|
-
async function readActionErrorBody(response: Response): Promise<unknown> {
|
|
27
|
-
const contentType = response.headers.get('content-type') || '';
|
|
28
|
-
try {
|
|
29
|
-
if (contentType.includes('application/json')) {
|
|
30
|
-
return await response.json();
|
|
31
|
-
}
|
|
32
|
-
const text = await response.text();
|
|
33
|
-
return text || null;
|
|
34
|
-
} catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
21
|
/**
|
|
40
22
|
* Form composable for island pages. Uses fetch + page reload instead of the
|
|
41
23
|
* Void Router (which is not available in island mode).
|
|
@@ -90,18 +72,14 @@ export function useIslandForm(defaults: Record<string, unknown>): UntypedIslandF
|
|
|
90
72
|
clearError();
|
|
91
73
|
|
|
92
74
|
try {
|
|
93
|
-
const
|
|
94
|
-
method,
|
|
95
|
-
headers: { 'Content-Type': 'application/json' },
|
|
96
|
-
body: JSON.stringify(data),
|
|
97
|
-
});
|
|
75
|
+
const result = await submitIslandAction(url, method, data);
|
|
98
76
|
|
|
99
|
-
if (
|
|
100
|
-
window.location.href =
|
|
77
|
+
if (result.kind === 'redirect') {
|
|
78
|
+
window.location.href = result.url;
|
|
101
79
|
return;
|
|
102
80
|
}
|
|
103
81
|
|
|
104
|
-
if (
|
|
82
|
+
if (result.kind === 'success') {
|
|
105
83
|
wasSuccessful = true;
|
|
106
84
|
recentlySuccessful = true;
|
|
107
85
|
defaults_ = { ...data };
|
|
@@ -109,23 +87,10 @@ export function useIslandForm(defaults: Record<string, unknown>): UntypedIslandF
|
|
|
109
87
|
recentlySuccessful = false;
|
|
110
88
|
}, 2000);
|
|
111
89
|
window.location.reload();
|
|
90
|
+
} else if (result.kind === 'validation') {
|
|
91
|
+
Object.assign(errors, result.errors);
|
|
112
92
|
} else {
|
|
113
|
-
|
|
114
|
-
const actionError = new VoidActionError({
|
|
115
|
-
body,
|
|
116
|
-
status: response.status,
|
|
117
|
-
statusText: response.statusText,
|
|
118
|
-
url,
|
|
119
|
-
});
|
|
120
|
-
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
|
|
121
|
-
if (validationErrors) {
|
|
122
|
-
Object.assign(errors, validationErrors);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (categorizeActionError(response.status) === 'boundary') {
|
|
126
|
-
throw actionError;
|
|
127
|
-
}
|
|
128
|
-
error = actionError;
|
|
93
|
+
error = result.error;
|
|
129
94
|
}
|
|
130
95
|
} finally {
|
|
131
96
|
pending = false;
|