@void/svelte 0.26.0 → 0.26.1
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/README.md +9 -5
- package/dist/plugin.d.mts +3 -1
- package/dist/plugin.mjs +23 -29
- package/package.json +12 -6
- package/src/runtime/Link.svelte +1 -63
- package/src/runtime/use-form.svelte.ts +6 -35
- package/src/runtime/use-island-form.svelte.ts +7 -16
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @void/svelte
|
|
2
2
|
|
|
3
|
-
Build server-rendered
|
|
3
|
+
Svelte adapter for [Void](https://void.cloud). Build server-rendered pages with typed server loaders, forms, layouts, and interactive islands.
|
|
4
|
+
|
|
5
|
+
Requires Svelte 5.
|
|
4
6
|
|
|
5
7
|
## Setup
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
For a new app, run `npx void init` and choose Svelte. To add the adapter to an existing Void app:
|
|
8
10
|
|
|
9
11
|
```sh
|
|
10
12
|
npm install @void/svelte
|
|
@@ -13,15 +15,17 @@ npm install @void/svelte
|
|
|
13
15
|
Add the adapter to your Vite config. It includes the framework's Vite plugin:
|
|
14
16
|
|
|
15
17
|
```ts
|
|
18
|
+
import { voidSvelte } from '@void/svelte/plugin';
|
|
16
19
|
import { defineConfig } from 'vite';
|
|
17
20
|
import { voidPlugin } from 'void';
|
|
18
|
-
import { voidSvelte } from '@void/svelte/plugin';
|
|
19
21
|
|
|
20
22
|
export default defineConfig({
|
|
21
23
|
plugins: [voidPlugin(), voidSvelte()],
|
|
22
24
|
});
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
Follow [Pages Routing](https://void.cloud/guide/pages-routing/overview) to add pages, then use [loaders](https://void.cloud/guide/pages-routing/loaders), [forms](https://void.cloud/guide/pages-routing/actions-and-forms), and [layouts](https://void.cloud/guide/pages-routing/layouts). See the [adapter API reference](https://void.cloud/reference/api/pages) for plugin options and runtime helpers.
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
Read the [Void documentation](https://void.cloud) online or use the Markdown guides and references in `node_modules/void/docs/`, included with the `void` package.
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
|
-
//#region src/plugin.d.ts
|
|
3
|
+
//#region src/plugin-pages.d.ts
|
|
4
4
|
type PrefetchConfig = {
|
|
5
5
|
hoverDelay?: number;
|
|
6
6
|
cacheFor?: number | string | [string, string];
|
|
7
7
|
};
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/plugin.d.ts
|
|
8
10
|
export type VoidSvelteOptions = Readonly<{
|
|
9
11
|
svelte?: Parameters<typeof svelte>[0];
|
|
10
12
|
viewTransitions?: boolean;
|
package/dist/plugin.mjs
CHANGED
|
@@ -63,9 +63,23 @@ function islandsPlugin(pagesDir, options) {
|
|
|
63
63
|
let isDev = false;
|
|
64
64
|
let viteRoot = "";
|
|
65
65
|
let resolvedConfig = null;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
/** Resolve an island import and record it in the client manifest. */
|
|
67
|
+
function registerIsland(spec, importerDir, hydrate) {
|
|
68
|
+
const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
|
|
69
|
+
if (!absPath) return {
|
|
70
|
+
absPath,
|
|
71
|
+
islandId: spec
|
|
72
|
+
};
|
|
73
|
+
const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
74
|
+
islandManifest.set(islandId, {
|
|
75
|
+
componentPath: absPath,
|
|
76
|
+
islandId,
|
|
77
|
+
hydrate
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
absPath,
|
|
81
|
+
islandId
|
|
82
|
+
};
|
|
69
83
|
}
|
|
70
84
|
return [{
|
|
71
85
|
name: "void-svelte:islands",
|
|
@@ -102,19 +116,7 @@ function islandsPlugin(pagesDir, options) {
|
|
|
102
116
|
const filePath = resolve(pagesDir, entry.filePath);
|
|
103
117
|
const code = readFileSync(filePath, "utf-8");
|
|
104
118
|
const importerDir = dirname(filePath);
|
|
105
|
-
for (const islandImport of findIslandImports(code, filePath))
|
|
106
|
-
const spec = islandImport.specifier;
|
|
107
|
-
const strategy = islandImport.hydrate;
|
|
108
|
-
const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
|
|
109
|
-
if (absPath) {
|
|
110
|
-
const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
111
|
-
islandManifest.set(islandId, {
|
|
112
|
-
componentPath: absPath,
|
|
113
|
-
islandId,
|
|
114
|
-
hydrate: strategy
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
119
|
+
for (const islandImport of findIslandImports(code, filePath)) registerIsland(islandImport.specifier, importerDir, islandImport.hydrate);
|
|
118
120
|
}
|
|
119
121
|
for (const page of pageScan.pages) {
|
|
120
122
|
if (!page.island) continue;
|
|
@@ -146,7 +148,7 @@ function islandsPlugin(pagesDir, options) {
|
|
|
146
148
|
if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
|
|
147
149
|
const mdPlugin = resolvedConfig?.plugins?.find((plugin) => plugin.name === "void-md");
|
|
148
150
|
const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
|
|
149
|
-
const needsJS = computePagesNeedingClientJS
|
|
151
|
+
const needsJS = computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
|
|
150
152
|
return generateIslandRenderer(pageScan, pagesDir, mdScriptIds, needsJS);
|
|
151
153
|
}
|
|
152
154
|
if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
|
|
@@ -170,16 +172,7 @@ function islandsPlugin(pagesDir, options) {
|
|
|
170
172
|
const name = islandImport.importName;
|
|
171
173
|
const spec = islandImport.specifier;
|
|
172
174
|
const strategy = islandImport.hydrate;
|
|
173
|
-
const absPath =
|
|
174
|
-
if (absPath) {
|
|
175
|
-
const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
176
|
-
islandManifest.set(islandId, {
|
|
177
|
-
componentPath: absPath,
|
|
178
|
-
islandId,
|
|
179
|
-
hydrate: strategy
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
const islandId = absPath ? normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "") : spec;
|
|
175
|
+
const { absPath, islandId } = registerIsland(spec, importerDir, strategy);
|
|
183
176
|
const wrapperUrl = WRAPPER_PREFIX + encodeURIComponent(islandId) + ".svelte?src=" + encodeURIComponent(absPath || spec) + "&island=" + encodeURIComponent(islandId) + "&hydrate=" + encodeURIComponent(strategy);
|
|
184
177
|
const replacement = `import ${name} from ${JSON.stringify(wrapperUrl)}`;
|
|
185
178
|
edits.push({
|
|
@@ -201,6 +194,7 @@ function islandsPlugin(pagesDir, options) {
|
|
|
201
194
|
}
|
|
202
195
|
}, {
|
|
203
196
|
name: "void-svelte:islands-style",
|
|
197
|
+
apply: "build",
|
|
204
198
|
resolveId: {
|
|
205
199
|
filter: { id: /\?island-style&/ },
|
|
206
200
|
handler(source) {
|
|
@@ -399,7 +393,7 @@ function pagesPlugin(pagesDir, options) {
|
|
|
399
393
|
if (!pageScan && existsSync(pagesDir)) await runRescan();
|
|
400
394
|
if (id === "\0" + RENDERER_ID) {
|
|
401
395
|
if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
|
|
402
|
-
return generateRendererModule(pageScan, pagesDir
|
|
396
|
+
return generateRendererModule(pageScan, pagesDir);
|
|
403
397
|
}
|
|
404
398
|
if (id === "\0" + CLIENT_ENTRY_ID) {
|
|
405
399
|
if (!pageScan) return "";
|
|
@@ -439,7 +433,7 @@ function pagesPlugin(pagesDir, options) {
|
|
|
439
433
|
}
|
|
440
434
|
};
|
|
441
435
|
}
|
|
442
|
-
function generateRendererModule(scan, pagesDir
|
|
436
|
+
function generateRendererModule(scan, pagesDir) {
|
|
443
437
|
return `
|
|
444
438
|
import { render } from "svelte/server";
|
|
445
439
|
import App from "@void/svelte/App.svelte";
|
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/svelte",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
4
|
+
"description": "Svelte pages, server rendering, forms, and navigation for Void.",
|
|
5
|
+
"homepage": "https://void.cloud",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/voidzero-dev/void/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
4
10
|
"repository": {
|
|
5
11
|
"type": "git",
|
|
6
12
|
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
@@ -34,15 +40,15 @@
|
|
|
34
40
|
"devDependencies": {
|
|
35
41
|
"@types/node": "^26.6.4",
|
|
36
42
|
"pathslash": "^0.1.0",
|
|
37
|
-
"svelte": "^5.57.
|
|
38
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
39
|
-
"vite-plus": "1.
|
|
40
|
-
"void": "0.26.
|
|
43
|
+
"svelte": "^5.57.2",
|
|
44
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
45
|
+
"vite-plus": "1.1.0",
|
|
46
|
+
"void": "0.26.1"
|
|
41
47
|
},
|
|
42
48
|
"peerDependencies": {
|
|
43
49
|
"svelte": "^5.0.0",
|
|
44
50
|
"vite": "^8.0.0",
|
|
45
|
-
"void": "0.26.
|
|
51
|
+
"void": "0.26.1"
|
|
46
52
|
},
|
|
47
53
|
"scripts": {
|
|
48
54
|
"build": "vp pack",
|
package/src/runtime/Link.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext, onDestroy, onMount, type Snippet } from "svelte";
|
|
3
|
-
import type
|
|
3
|
+
import { isModifiedEvent, mergeDataIntoHref, type VoidRouter } from "void/pages-client";
|
|
4
4
|
|
|
5
5
|
type PrefetchStrategy = "hover" | "click" | "mount" | "visible";
|
|
6
6
|
type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -57,72 +57,10 @@
|
|
|
57
57
|
|
|
58
58
|
const router = getContext<VoidRouter>("__void_router");
|
|
59
59
|
|
|
60
|
-
function appendQueryValue(params: URLSearchParams, key: string, value: unknown): boolean {
|
|
61
|
-
if (value === null || value === undefined) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
if (value instanceof Date) {
|
|
65
|
-
params.append(key, value.toISOString());
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
let appended = false;
|
|
70
|
-
for (const item of value) {
|
|
71
|
-
appended = appendQueryValue(params, key, item) || appended;
|
|
72
|
-
}
|
|
73
|
-
return appended;
|
|
74
|
-
}
|
|
75
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
76
|
-
params.append(key, String(value));
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Link: GET data only supports primitive values and arrays. Remove nested data for '${key}'.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function mergeDataIntoHref(hrefValue: string, dataValue?: Record<string, unknown>): string {
|
|
85
|
-
if (!dataValue) {
|
|
86
|
-
return hrefValue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const hashIndex = hrefValue.indexOf("#");
|
|
90
|
-
const beforeHash = hashIndex === -1 ? hrefValue : hrefValue.slice(0, hashIndex);
|
|
91
|
-
const hash = hashIndex === -1 ? "" : hrefValue.slice(hashIndex);
|
|
92
|
-
const searchIndex = beforeHash.indexOf("?");
|
|
93
|
-
const path = searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex);
|
|
94
|
-
const search = searchIndex === -1 ? "" : beforeHash.slice(searchIndex + 1);
|
|
95
|
-
const params = new URLSearchParams(search);
|
|
96
|
-
let appended = false;
|
|
97
|
-
|
|
98
|
-
for (const [key, value] of Object.entries(dataValue)) {
|
|
99
|
-
appended = appendQueryValue(params, key, value) || appended;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!appended) {
|
|
103
|
-
return hrefValue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const query = params.toString();
|
|
107
|
-
return `${path}${query ? `?${query}` : ""}${hash}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
60
|
function hasPrefetch(prefetch: boolean | PrefetchStrategy | Array<PrefetchStrategy>): boolean {
|
|
111
61
|
return prefetch !== false;
|
|
112
62
|
}
|
|
113
63
|
|
|
114
|
-
function isModifiedEvent(e: MouseEvent): boolean {
|
|
115
|
-
const target = (e.currentTarget as HTMLElement).getAttribute("target");
|
|
116
|
-
return (
|
|
117
|
-
(target !== null && target !== "_self") ||
|
|
118
|
-
e.ctrlKey ||
|
|
119
|
-
e.metaKey ||
|
|
120
|
-
e.shiftKey ||
|
|
121
|
-
e.altKey ||
|
|
122
|
-
e.button !== 0
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
64
|
function getStrategies(): Array<PrefetchStrategy> {
|
|
127
65
|
if (hasPrefetch(prefetchProp) && !isGet) {
|
|
128
66
|
throw new Error(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getContext } from 'svelte';
|
|
2
2
|
import {
|
|
3
3
|
VoidActionError,
|
|
4
|
+
getValidationErrors,
|
|
5
|
+
interpolateRouteParams,
|
|
4
6
|
isAbortError,
|
|
5
7
|
isEqualFormValue,
|
|
6
8
|
submitAction,
|
|
@@ -29,21 +31,6 @@ type UntypedForm = {
|
|
|
29
31
|
clearError(): void;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
|
-
function getValidationErrors(error: VoidActionError): Record<string, string> | null {
|
|
33
|
-
const body = error.body;
|
|
34
|
-
if (
|
|
35
|
-
body &&
|
|
36
|
-
typeof body === 'object' &&
|
|
37
|
-
'errors' in body &&
|
|
38
|
-
body.errors &&
|
|
39
|
-
typeof body.errors === 'object' &&
|
|
40
|
-
!Array.isArray(body.errors)
|
|
41
|
-
) {
|
|
42
|
-
return body.errors as Record<string, string>;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
34
|
// Typed overload — full inference from PageActionMap (requires codegen)
|
|
48
35
|
export function useForm<U extends ActionUrl & string>(
|
|
49
36
|
url: U,
|
|
@@ -71,24 +58,6 @@ export function useForm(
|
|
|
71
58
|
defaults: Record<string, unknown>,
|
|
72
59
|
options?: { params?: Record<string, string> },
|
|
73
60
|
): UntypedForm {
|
|
74
|
-
let resolvedUrl = url;
|
|
75
|
-
|
|
76
|
-
// Strip ?action query before param resolution, then re-append
|
|
77
|
-
let actionQuery = '';
|
|
78
|
-
const qIdx = resolvedUrl.indexOf('?');
|
|
79
|
-
if (qIdx !== -1) {
|
|
80
|
-
actionQuery = resolvedUrl.slice(qIdx);
|
|
81
|
-
resolvedUrl = resolvedUrl.slice(0, qIdx);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (options?.params) {
|
|
85
|
-
for (const [key, value] of Object.entries(options.params)) {
|
|
86
|
-
resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
resolvedUrl = resolvedUrl + actionQuery;
|
|
91
|
-
|
|
92
61
|
const router = getContext<VoidRouter>('__void_router');
|
|
93
62
|
|
|
94
63
|
let data = $state({ ...defaults });
|
|
@@ -140,14 +109,16 @@ export function useForm(
|
|
|
140
109
|
clearError();
|
|
141
110
|
|
|
142
111
|
try {
|
|
143
|
-
|
|
112
|
+
// Params are filled in on submit, so a param that would change the
|
|
113
|
+
// request path (such as "..") fails this submit, not the page render.
|
|
114
|
+
const result = await submitAction(router, interpolateRouteParams(url, options?.params), {
|
|
144
115
|
method,
|
|
145
116
|
data: { ...data },
|
|
146
117
|
...opts,
|
|
147
118
|
});
|
|
148
119
|
|
|
149
120
|
if (!result.ok) {
|
|
150
|
-
const validationErrors = getValidationErrors(result.error);
|
|
121
|
+
const validationErrors = getValidationErrors(result.error.body);
|
|
151
122
|
if (validationErrors) {
|
|
152
123
|
Object.assign(errors, validationErrors);
|
|
153
124
|
} else {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
VoidActionError,
|
|
3
|
+
categorizeActionError,
|
|
4
|
+
getValidationErrors,
|
|
5
|
+
isEqualFormValue,
|
|
6
|
+
} from 'void/pages-client';
|
|
2
7
|
|
|
3
8
|
type UntypedIslandForm = {
|
|
4
9
|
readonly data: Record<string, unknown>;
|
|
@@ -18,20 +23,6 @@ type UntypedIslandForm = {
|
|
|
18
23
|
clearError(): void;
|
|
19
24
|
};
|
|
20
25
|
|
|
21
|
-
function getValidationErrors(body: unknown): Record<string, string> | null {
|
|
22
|
-
if (
|
|
23
|
-
body &&
|
|
24
|
-
typeof body === 'object' &&
|
|
25
|
-
'errors' in body &&
|
|
26
|
-
body.errors &&
|
|
27
|
-
typeof body.errors === 'object' &&
|
|
28
|
-
!Array.isArray(body.errors)
|
|
29
|
-
) {
|
|
30
|
-
return body.errors as Record<string, string>;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
async function readActionErrorBody(response: Response): Promise<unknown> {
|
|
36
27
|
const contentType = response.headers.get('content-type') || '';
|
|
37
28
|
try {
|
|
@@ -118,7 +109,7 @@ export function useIslandForm(defaults: Record<string, unknown>): UntypedIslandF
|
|
|
118
109
|
recentlySuccessful = false;
|
|
119
110
|
}, 2000);
|
|
120
111
|
window.location.reload();
|
|
121
|
-
} else
|
|
112
|
+
} else {
|
|
122
113
|
const body = await readActionErrorBody(response);
|
|
123
114
|
const actionError = new VoidActionError({
|
|
124
115
|
body,
|