@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 CHANGED
@@ -1,10 +1,12 @@
1
1
  # @void/svelte
2
2
 
3
- Build server-rendered Void pages with Svelte 5, server loaders, and typed forms.
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
- Install alongside `void`:
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
- 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).
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
- See the [adapter API reference](https://void.cloud/reference/api/pages) for plugin options and runtime helpers, and [TESTING.md](../../TESTING.md) for repository verification.
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
- function computePagesNeedingClientJS$1() {
67
- if (!pageScan) return /* @__PURE__ */ new Set();
68
- return computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
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$1();
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 = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
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, isDev);
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, _isDev) {
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.0",
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.1",
38
- "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
39
- "vite-plus": "1.0.0",
40
- "void": "0.26.0"
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.0"
51
+ "void": "0.26.1"
46
52
  },
47
53
  "scripts": {
48
54
  "build": "vp pack",
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onDestroy, onMount, type Snippet } from "svelte";
3
- import type { VoidRouter } from "void/pages-client";
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
- const result = await submitAction(router, resolvedUrl, {
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 { VoidActionError, categorizeActionError, isEqualFormValue } from 'void/pages-client';
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 if (!response.ok) {
112
+ } else {
122
113
  const body = await readActionErrorBody(response);
123
114
  const actionError = new VoidActionError({
124
115
  body,