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