@void/vue 1.5.0 → 1.6.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,50 +1,23 @@
1
- import { ActionClient, FormResetOptions, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
2
- import { Component } from "vue";
1
+ import { ActionClient, BaseLinkProps, FormState, FormSubmitOptions, IslandFormState, NavigationState, VoidRouter } from "void/pages-client";
2
+ import { AnchorHTMLAttributes, ButtonHTMLAttributes, Component, DefineSetupFnComponent } from "vue";
3
3
  import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
4
4
  import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
5
5
  export declare const Form: Component;
6
- export declare const Link: Component;
6
+ type LinkProps = BaseLinkProps & Omit<AnchorHTMLAttributes, keyof BaseLinkProps> & Omit<ButtonHTMLAttributes, keyof BaseLinkProps>;
7
+ export declare const Link: DefineSetupFnComponent<LinkProps>;
7
8
  export declare function useRouter(): VoidRouter;
8
9
  export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
9
10
  export declare function useShared<T = CloudContextVariables["shared"]>(): T;
10
- type FormSubmitOptions = Omit<VisitOptions, "method" | "data"> & {
11
- data?: Record<string, unknown>;
12
- };
13
- export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
14
- data: ResolveActionBody<U>;
15
- errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
16
- error: VoidActionError | null;
17
- pending: boolean;
18
- hasChanges: boolean;
19
- wasSuccessful: boolean;
20
- recentlySuccessful: boolean;
11
+ type FormReturn<TData> = FormState<TData, "in-place"> & Readonly<{
21
12
  post(opts?: FormSubmitOptions): Promise<void>;
22
13
  put(opts?: FormSubmitOptions): Promise<void>;
23
14
  patch(opts?: FormSubmitOptions): Promise<void>;
24
15
  delete(opts?: FormSubmitOptions): Promise<void>;
25
- reset(options: FormResetOptions, ...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
26
- reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
27
- setError(field: keyof ResolveActionBody<U> & string, message: string): void;
28
- clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
29
- clearError(): void;
30
- };
16
+ }>;
17
+ export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): FormReturn<ResolveActionBody<U>>;
31
18
  export declare function useNavigation(): NavigationState;
32
- type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
33
- data: T;
34
- errors: Record<string, string>;
35
- error: VoidActionError | null;
36
- pending: boolean;
37
- hasChanges: boolean;
38
- wasSuccessful: boolean;
39
- recentlySuccessful: boolean;
19
+ type IslandFormReturn<T extends Record<string, unknown>> = IslandFormState<T, "in-place"> & Readonly<{
40
20
  submit(url: string, method: string): Promise<void>;
41
- post(url: string): Promise<void>;
42
- put(url: string): Promise<void>;
43
- patch(url: string): Promise<void>;
44
- delete(url: string): Promise<void>;
45
- reset(...fields: ReadonlyArray<keyof T & string>): void;
46
- clearErrors(...fields: ReadonlyArray<string>): void;
47
- clearError(): void;
48
21
  }>;
49
22
  /**
50
23
  * Form composable for island pages. Uses fetch + page reload instead of the
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { cloneFormValue, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, parseFormResetArgs, scheduleFormRetry, ssrProxy, submitAction, submitIslandAction } from "void/pages-client";
1
+ import { cloneFormValue, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, handleLinkClick, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, linkActiveAttributes, linkPendingAttributes, linkPrefetchStrategies, observeLinkVisible, parseFormResetArgs, prefetchLink, resolveLink, scheduleFormRetry, ssrProxy, submitAction, submitIslandAction } from "void/pages-client";
2
2
  import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from "vue";
3
3
  // src/runtime/form.ts
4
4
  const Form = defineComponent({
@@ -43,6 +43,64 @@ const Form = defineComponent({
43
43
  }
44
44
  });
45
45
  // src/runtime/link.ts
46
+ const linkPropOptions = {
47
+ href: {
48
+ type: String,
49
+ required: true
50
+ },
51
+ method: {
52
+ type: String,
53
+ default: "GET"
54
+ },
55
+ data: {
56
+ type: Object,
57
+ default: void 0
58
+ },
59
+ preserveScroll: {
60
+ type: Boolean,
61
+ default: false
62
+ },
63
+ preserveState: {
64
+ type: Boolean,
65
+ default: false
66
+ },
67
+ replace: {
68
+ type: Boolean,
69
+ default: false
70
+ },
71
+ reloadDocument: {
72
+ type: Boolean,
73
+ default: false
74
+ },
75
+ viewTransition: {
76
+ type: Boolean,
77
+ default: void 0
78
+ },
79
+ onNavigate: {
80
+ type: Function,
81
+ default: void 0
82
+ },
83
+ prefetch: {
84
+ type: [
85
+ Boolean,
86
+ String,
87
+ Array
88
+ ],
89
+ default: void 0
90
+ },
91
+ cacheFor: {
92
+ type: [
93
+ Number,
94
+ String,
95
+ Array
96
+ ],
97
+ default: void 0
98
+ },
99
+ activeMatch: {
100
+ type: String,
101
+ default: void 0
102
+ }
103
+ };
46
104
  function callHandler(handler, event) {
47
105
  if (Array.isArray(handler)) {
48
106
  for (const fn of handler) if (typeof fn === "function") fn(event);
@@ -50,108 +108,28 @@ function callHandler(handler, event) {
50
108
  }
51
109
  const Link = defineComponent({
52
110
  name: "Link",
53
- props: {
54
- href: {
55
- type: String,
56
- required: true
57
- },
58
- method: {
59
- type: String,
60
- default: "GET"
61
- },
62
- data: {
63
- type: Object,
64
- default: void 0
65
- },
66
- preserveScroll: {
67
- type: Boolean,
68
- default: false
69
- },
70
- preserveState: {
71
- type: Boolean,
72
- default: false
73
- },
74
- replace: {
75
- type: Boolean,
76
- default: false
77
- },
78
- reloadDocument: {
79
- type: Boolean,
80
- default: false
81
- },
82
- viewTransition: {
83
- type: Boolean,
84
- default: void 0
85
- },
86
- onNavigate: {
87
- type: Function,
88
- default: void 0
89
- },
90
- prefetch: {
91
- type: [
92
- Boolean,
93
- String,
94
- Array
95
- ],
96
- default: false
97
- },
98
- cacheFor: {
99
- type: [
100
- Number,
101
- String,
102
- Array
103
- ],
104
- default: void 0
105
- }
106
- },
111
+ props: linkPropOptions,
107
112
  setup(props, { slots, attrs }) {
108
113
  const router = inject("__void_router", null);
114
+ const navigation = inject("__void_navigation", null);
109
115
  const elRef = ref(null);
110
- const normalizedMethod = computed(() => props.method.toUpperCase());
111
- const isGet = computed(() => normalizedMethod.value === "GET");
112
- const hrefWithData = computed(() => isGet.value ? mergeDataIntoHref(props.href, props.data) : props.href);
113
- const strategies = computed(() => {
114
- if (props.prefetch === false || props.reloadDocument) return [];
115
- if (props.prefetch === true) return ["hover"];
116
- if (typeof props.prefetch === "string") return [props.prefetch];
117
- return props.prefetch;
118
- });
116
+ const link = computed(() => resolveLink(props));
117
+ const strategies = computed(() => linkPrefetchStrategies(props, link.value, router?._prefetchDefaultStrategy));
118
+ const activeAttributes = computed(() => linkActiveAttributes(router, link.value, props.activeMatch));
119
+ const pendingAttributes = computed(() => linkPendingAttributes(router, navigation, link.value));
119
120
  function doPrefetch() {
120
- if (!router?.prefetch) return;
121
- const current = window.location.pathname + window.location.search;
122
- if (hrefWithData.value === current) return;
123
- const options = { method: normalizedMethod.value };
124
- if (props.cacheFor !== void 0) options.cacheFor = props.cacheFor;
125
- router.prefetch(hrefWithData.value, options);
121
+ prefetchLink(router, link.value.href, props.cacheFor);
126
122
  }
127
123
  function onClick(e) {
128
124
  callHandler(attrs.onClick, e);
129
- if (props.reloadDocument || !router || e.defaultPrevented || isModifiedEvent(e)) return;
130
- if (e.currentTarget.hasAttribute("download")) return;
131
- if (new URL(hrefWithData.value, window.location.origin).origin !== window.location.origin) return;
132
- let navigatePrevented = false;
133
- props.onNavigate?.({ preventDefault() {
134
- navigatePrevented = true;
135
- } });
136
- if (navigatePrevented) {
137
- e.preventDefault();
138
- return;
139
- }
140
- e.preventDefault();
141
- router.visit(hrefWithData.value, {
142
- method: normalizedMethod.value,
143
- data: isGet.value ? void 0 : props.data,
144
- preserveScroll: props.preserveScroll,
145
- preserveState: props.preserveState,
146
- replace: props.replace,
147
- viewTransition: props.viewTransition
148
- });
125
+ handleLinkClick(e, router, link.value, props);
149
126
  }
150
127
  let hoverTimer = null;
151
- let observer = null;
128
+ let stopObserving = null;
152
129
  function onMouseEnter(e) {
153
130
  callHandler(attrs.onMouseenter ?? attrs.onMouseEnter, e);
154
131
  if (e.defaultPrevented) return;
132
+ if (hoverTimer) clearTimeout(hoverTimer);
155
133
  hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
156
134
  }
157
135
  function onMouseLeave(e) {
@@ -175,28 +153,15 @@ const Link = defineComponent({
175
153
  }
176
154
  onMounted(() => {
177
155
  if (strategies.value.includes("mount")) doPrefetch();
178
- if (strategies.value.includes("visible") && elRef.value) {
179
- observer = new IntersectionObserver((entries) => {
180
- if (entries.some((e) => e.isIntersecting)) {
181
- doPrefetch();
182
- observer?.disconnect();
183
- observer = null;
184
- }
185
- }, { rootMargin: "0px" });
186
- observer.observe(elRef.value);
187
- }
156
+ if (strategies.value.includes("visible") && elRef.value) stopObserving = observeLinkVisible(elRef.value, doPrefetch);
188
157
  });
189
158
  onUnmounted(() => {
190
159
  if (hoverTimer) clearTimeout(hoverTimer);
191
- if (observer) {
192
- observer.disconnect();
193
- observer = null;
194
- }
160
+ stopObserving?.();
195
161
  });
196
162
  return () => {
197
- if (props.prefetch !== false && !isGet.value) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
198
- if (props.reloadDocument && !isGet.value) throw new Error("Link: reloadDocument only supports GET links.");
199
- const tag = isGet.value ? "a" : "button";
163
+ const { href, isGet } = link.value;
164
+ const tag = isGet ? "a" : "button";
200
165
  const eventHandlers = { onClick };
201
166
  if (strategies.value.includes("hover")) {
202
167
  eventHandlers.onMouseenter = onMouseEnter;
@@ -209,9 +174,11 @@ const Link = defineComponent({
209
174
  eventHandlers.onTouchstart = onTouchStart;
210
175
  }
211
176
  return h(tag, {
177
+ ...activeAttributes.value,
178
+ ...pendingAttributes.value,
212
179
  ...attrs,
213
180
  ref: elRef,
214
- ...isGet.value ? { href: hrefWithData.value } : { type: "button" },
181
+ ...isGet ? { href } : { type: "button" },
215
182
  style: [{ touchAction: "manipulation" }, attrs.style],
216
183
  ...eventHandlers
217
184
  }, slots.default?.());
@@ -328,10 +295,6 @@ function useNavigation() {
328
295
  return inject("__void_navigation", null) ?? idleNavigationState();
329
296
  }
330
297
  // src/runtime/use-island-form.ts
331
- /**
332
- * Form composable for island pages. Uses fetch + page reload instead of the
333
- * Void Router (which is not available in island mode).
334
- */
335
298
  function useIslandForm(defaults) {
336
299
  const data = reactive({ ...defaults });
337
300
  const errors = reactive({});
package/dist/plugin.d.mts CHANGED
@@ -1,13 +1,9 @@
1
1
  import vue from "@vitejs/plugin-vue";
2
+ import { PrefetchConfig } from "void/pages";
2
3
  import { Plugin } from "vite";
3
- type PrefetchConfig = {
4
- hoverDelay?: number;
5
- cacheFor?: number | string | [string, string];
6
- };
7
4
  export type VoidVueOptions = Readonly<{
8
5
  vue?: Parameters<typeof vue>[0];
9
6
  viewTransitions?: boolean;
10
7
  prefetch?: PrefetchConfig;
11
8
  }>;
12
- export declare function voidVue(options?: VoidVueOptions): Array<Plugin>;
13
- export {};
9
+ export declare function voidVue(options?: VoidVueOptions): Array<Plugin>;
package/dist/plugin.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import vue from "@vitejs/plugin-vue";
2
- import { createPagesPlugin, generateComponentManifest, resolveEffectiveLayoutChain, resolvePagesDir, scanPages } from "void/pages";
3
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { createPagesPlugin, generateComponentManifest, resolvePagesDir, resolvePrefetchConfig, scanPages } from "void/pages";
3
+ import { existsSync, readdirSync } from "node:fs";
4
4
  import nodePath from "node:path";
5
+ import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, createIslandManifest, editIslandSource, findIslandImports, generateIslandClientEntry, islandCssFiles, islandStylesPlugin, islandsBuildConfig, mdClientScriptIds, recordIslandStyles, recordedIslandStyles, watchPagesForRescan } from "void/pages-islands-plugin";
5
6
  import { normalizePath } from "vite";
6
- import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, editIslandSource, findIslandImports, findStaticCssImports, hasIslandPages, islandStylesPlugin, recordIslandStyles, recordedIslandStyles, resolveSpecifier, rewriteCssImportForVirtualModule, watchPagesForRescan } from "void/pages-islands-plugin";
7
7
  // ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
8
8
  const BACKSLASH_RE = /\\/g;
9
9
  const isWindows = process.platform === "win32";
@@ -50,30 +50,36 @@ const COMPONENT_EXTENSIONS = [
50
50
  ".ts",
51
51
  ".js"
52
52
  ];
53
+ const HYDRATE_ISLAND = `
54
+ async function hydrateIsland(el, mod, props, frontmatter) {
55
+ const { createSSRApp, h } = await import("vue");
56
+ const Component = mod.default || mod;
57
+ const app = createSSRApp({ render: () => h(Component, props) });
58
+ app.provide("__void_frontmatter", frontmatter);
59
+ app.mount(el);
60
+ el.setAttribute("data-hydrated", "true");
61
+ }`;
62
+ /** Imports for the `<style>` blocks of a Vue component. */
63
+ function styleBlockImports(file, content) {
64
+ const imports = [];
65
+ let index = 0;
66
+ for (const match of content.matchAll(/<style\b([^>]*)>/g)) {
67
+ const attrs = match[1];
68
+ const lang = attrs.match(/\blang\s*=\s*["'](\w+)["']/)?.[1] ?? "css";
69
+ let query = `?vue&type=style&index=${index}&lang.${lang}`;
70
+ if (/\bscoped\b/.test(attrs)) query += "&scoped=true";
71
+ imports.push(`import ${JSON.stringify(file + query)};`);
72
+ index++;
73
+ }
74
+ return imports;
75
+ }
53
76
  function islandsPlugin(pagesDir, options) {
54
77
  let pageScan = null;
55
78
  const root = options?.root ?? dirname(pagesDir);
56
- const islandManifest = /* @__PURE__ */ new Map();
57
- const islandCssFiles = /* @__PURE__ */ new Set();
79
+ const islandManifest = createIslandManifest(root, COMPONENT_EXTENSIONS);
80
+ let cssFiles = /* @__PURE__ */ new Set();
58
81
  let isDev = false;
59
82
  let resolvedConfig = null;
60
- function registerIsland(spec, importerDir, hydrate) {
61
- const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
62
- if (!absPath) return {
63
- absPath,
64
- islandId: spec
65
- };
66
- const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
67
- islandManifest.set(islandId, {
68
- componentPath: absPath,
69
- islandId,
70
- hydrate
71
- });
72
- return {
73
- absPath,
74
- islandId
75
- };
76
- }
77
83
  return [{
78
84
  name: "void-vue:islands",
79
85
  enforce: "pre",
@@ -92,33 +98,8 @@ function islandsPlugin(pagesDir, options) {
92
98
  pagesDir,
93
99
  environment: isDev ? "dev" : "prod"
94
100
  });
95
- const filesToScan = [
96
- ...pageScan.pages.map((page) => ({
97
- filePath: page.componentPath,
98
- island: page.island
99
- })),
100
- ...pageScan.layouts.map((layout) => ({
101
- filePath: layout.filePath,
102
- island: layout.island
103
- })),
104
- ...pageScan.namedLayouts.map((namedLayout) => ({
105
- filePath: namedLayout.filePath,
106
- island: namedLayout.island
107
- }))
108
- ];
109
- for (const entry of filesToScan) {
110
- if (!entry.island) continue;
111
- const filePath = resolve(pagesDir, entry.filePath);
112
- const code = readFileSync(filePath, "utf-8");
113
- const importerDir = dirname(filePath);
114
- for (const islandImport of findIslandImports(code, filePath)) registerIsland(islandImport.specifier, importerDir, islandImport.hydrate);
115
- }
116
- for (const page of pageScan.pages) {
117
- if (!page.island) continue;
118
- if (page.componentPath.endsWith(".vue")) islandCssFiles.add(normalizePath(resolve(pagesDir, page.componentPath)));
119
- const layoutChain = resolveEffectiveLayoutChain(page, pageScan.layouts, pageScan.namedLayouts);
120
- for (const layout of layoutChain) islandCssFiles.add(normalizePath(resolve(pagesDir, layout.filePath)));
121
- }
101
+ islandManifest.registerPages(pageScan, pagesDir);
102
+ cssFiles = islandCssFiles(pageScan, pagesDir, /\.vue$/);
122
103
  }
123
104
  },
124
105
  buildEnd() {
@@ -140,14 +121,21 @@ function islandsPlugin(pagesDir, options) {
140
121
  handler(id) {
141
122
  if (id === "\0" + ISLANDS_RENDERER_ID) {
142
123
  if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
143
- const mdPlugin = resolvedConfig?.plugins?.find((plugin) => plugin.name === "void-md");
144
- const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
124
+ const mdScriptIds = mdClientScriptIds(resolvedConfig);
145
125
  const needsJS = computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
146
126
  return generateIslandRenderer(pageScan, pagesDir, mdScriptIds, needsJS);
147
127
  }
148
128
  if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
149
129
  if (!pageScan) return "";
150
- return generateIslandClientEntry(islandManifest, islandCssFiles, recordedIslandStyles(pagesDir), islandManifest.size > 0 || computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig).size > 0, isDev);
130
+ return generateIslandClientEntry({
131
+ manifest: islandManifest,
132
+ cssFiles,
133
+ styles: recordedIslandStyles(pagesDir),
134
+ needsRuntime: islandManifest.entries.size > 0 || computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig).size > 0,
135
+ isDev,
136
+ styleBlockImports,
137
+ hydrateIsland: HYDRATE_ISLAND
138
+ });
151
139
  }
152
140
  }
153
141
  },
@@ -165,7 +153,7 @@ function islandsPlugin(pagesDir, options) {
165
153
  const strategy = islandImport.hydrate;
166
154
  const spec = islandImport.specifier;
167
155
  const rawName = `_raw_island_${idx++}`;
168
- const { islandId } = registerIsland(spec, importerDir, strategy);
156
+ const { islandId } = islandManifest.register(spec, importerDir, strategy);
169
157
  const replacement = `import ${rawName} from ${specRaw}`;
170
158
  edits.push({
171
159
  start: islandImport.start,
@@ -193,10 +181,7 @@ function islandsPlugin(pagesDir, options) {
193
181
  async config(_, env) {
194
182
  if (!existsSync(pagesDir)) return;
195
183
  if (env.command === "serve") return { optimizeDeps: { include: ["vue"] } };
196
- if (await hasIslandPages(root, pagesDir)) return { environments: { client: { build: {
197
- manifest: true,
198
- rollupOptions: { input: { "islands-client": ISLANDS_CLIENT_ENTRY_ID } }
199
- } } } };
184
+ return islandsBuildConfig(root, pagesDir);
200
185
  }
201
186
  }, islandStylesPlugin(pagesDir)];
202
187
  }
@@ -265,51 +250,6 @@ export async function renderIslandPage(pageObj, assetTags) {
265
250
  }
266
251
  `;
267
252
  }
268
- function generateIslandClientEntry(manifest, cssFiles, styles, needsRuntime, isDev) {
269
- const entries = [...manifest.values()];
270
- const uniqueIslands = /* @__PURE__ */ new Map();
271
- for (const entry of entries) uniqueIslands.set(entry.islandId, entry.componentPath);
272
- const lines = [];
273
- if (!isDev && styles) for (const id of styles) lines.push(`import ${JSON.stringify(id)};`);
274
- else if (!isDev) {
275
- const styleRe = /<style\b([^>]*)>/g;
276
- for (const file of cssFiles) {
277
- const content = readFileSync(file, "utf-8");
278
- styleRe.lastIndex = 0;
279
- let index = 0;
280
- let match;
281
- while ((match = styleRe.exec(content)) !== null) {
282
- const attrs = match[1];
283
- const scoped = /\bscoped\b/.test(attrs);
284
- const langMatch = attrs.match(/\blang\s*=\s*["'](\w+)["']/);
285
- const lang = langMatch ? langMatch[1] : "css";
286
- let query = `?vue&type=style&index=${index}&lang.${lang}`;
287
- if (scoped) query += "&scoped=true";
288
- lines.push(`import ${JSON.stringify(file + query)};`);
289
- index++;
290
- }
291
- for (const item of findStaticCssImports(content, file)) lines.push(rewriteCssImportForVirtualModule(item.statement, item.specifier, file));
292
- }
293
- }
294
- if (!needsRuntime && !isDev) return lines.join("\n");
295
- lines.push("import { initIslands } from \"void/pages-client\";");
296
- lines.push("const islands = {");
297
- for (const [id, path] of uniqueIslands) lines.push(` ${JSON.stringify(id)}: () => import(${JSON.stringify(path)}),`);
298
- lines.push("};");
299
- lines.push(`
300
- async function hydrateIsland(el, mod, props, frontmatter) {
301
- const { createSSRApp, h } = await import("vue");
302
- const Component = mod.default || mod;
303
- const app = createSSRApp({ render: () => h(Component, props) });
304
- app.provide("__void_frontmatter", frontmatter);
305
- app.mount(el);
306
- el.setAttribute("data-hydrated", "true");
307
- }
308
-
309
- initIslands(islands, hydrateIsland);
310
- `);
311
- return lines.join("\n");
312
- }
313
253
  // src/plugin-pages.ts
314
254
  const CLIENT_ENTRY_ID = "virtual:void-pages-client";
315
255
  /** Runtime helper injected into generated SSR and client page bundles. */
@@ -444,7 +384,7 @@ ${generateComponentManifest(scan, pagesDir)}
444
384
 
445
385
  ${PATCH_PROPS_RUNTIME}
446
386
 
447
- export async function renderPage(pageObj, assetTags) {
387
+ export async function renderPage(pageObj, assetTags, requestUrl) {
448
388
  const PageComponent = (await components[pageObj.component]()).default;
449
389
  const layoutIds = layoutTree[pageObj.component] || [];
450
390
  const layoutComponents = await Promise.all(
@@ -455,7 +395,7 @@ export async function renderPage(pageObj, assetTags) {
455
395
  setup() {
456
396
  provide("__void_shared", reactive(pageObj.shared || {}));
457
397
  provide("__void_params", reactive(pageObj.params || {}));
458
- provide("__void_router", createSsrRouter(pageObj.url || "/", pageObj.params || {}));
398
+ provide("__void_router", createSsrRouter(pageObj.url || "/", pageObj.params || {}, requestUrl));
459
399
  provide("__void_navigation", idleNavigationState());
460
400
  return () => {
461
401
  patchProps(PageComponent, pageObj.props);
@@ -488,6 +428,7 @@ export async function renderPage(pageObj, assetTags) {
488
428
  `;
489
429
  }
490
430
  function generateClientEntry(scan, pagesDir, viewTransitions, prefetch, staticPageData) {
431
+ const { defaultStrategy, hoverDelay, cacheFor } = resolvePrefetchConfig(prefetch);
491
432
  return `
492
433
  import { createApp, createSSRApp, h, reactive, provide, shallowRef, nextTick, ref } from "vue";
493
434
  import { createDeferred, resolveDeferred, rejectDeferred } from "@void/vue/deferred";
@@ -534,8 +475,9 @@ const { router, initialPageData, initialComponent, initialLayouts, start, commit
534
475
  staticPageData: ${!!staticPageData?.enabled},
535
476
  staticPageDataFastPath: ${!!staticPageData?.fastPath},
536
477
  viewTransitions: ${!!viewTransitions},
537
- prefetchHoverDelay: ${prefetch?.hoverDelay ?? 75},
538
- prefetchDefaultCacheFor: parseCacheFor(${JSON.stringify(prefetch?.cacheFor ?? "30s")}),
478
+ prefetchHoverDelay: ${hoverDelay},
479
+ prefetchDefaultCacheFor: parseCacheFor(${JSON.stringify(cacheFor)}),
480
+ prefetchDefaultStrategy: ${JSON.stringify(defaultStrategy)},
539
481
  onRouteChange(url) {
540
482
  routeUrl.value = url;
541
483
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/vue",
3
- "version": "1.5.0",
3
+ "version": "1.6.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.4.0",
43
43
  "vite-plus": "1.4.0",
44
- "void": "1.5.0",
44
+ "void": "1.6.0",
45
45
  "vue": "^3.5.44"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "vite": "^8.0.0",
49
- "void": "1.5.0",
49
+ "void": "1.6.0",
50
50
  "vue": "^3.5.0"
51
51
  },
52
52
  "scripts": {