vike-lite-vue 0.1.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/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # vike-lite-vue
2
+
3
+ The official Vue integration for `vike-lite`. It provides seamless Server-Side Rendering (SSR), Static Site Generation (SSG), and client hydration out of the box, with a focus on minimalism and performance.
4
+
5
+ ### ⚙️ Install
6
+ You need to install both `vike-lite-vue` and the official Vite plugin for Vue (`@vitejs/plugin-vue`).
7
+
8
+ ```sh
9
+ # npm
10
+ npm install -D vike-lite-vue @vitejs/plugin-vue
11
+ npm install vue
12
+
13
+ # pnpm
14
+ pnpm add -D vike-lite-vue @vitejs/plugin-vue
15
+ pnpm add vue
16
+
17
+ # yarn
18
+ yarn add -D vike-lite-vue @vitejs/plugin-vue
19
+ yarn add vue
20
+ ```
21
+
22
+ ### ⚙️ Vite Plugin
23
+ Add the plugin to your `vite.config`.
24
+
25
+ ```ts
26
+ // vite.config.ts
27
+ import type { UserConfig } from 'vite'
28
+ import vikeLite from 'vike-lite/vite'
29
+ import vikeLiteVue from 'vike-lite-vue/vite'
30
+
31
+ export default {
32
+ plugins: [
33
+ vikeLite(),
34
+ vikeLiteVue({
35
+ // Default is `true` that enables Vue Hydration
36
+ // Set to `false` for Client Takeover (SPA mode)
37
+ hydration: true,
38
+ // Advanced: pass options directly to the underlying @vitejs/plugin-vue
39
+ vue: {
40
+ template: {
41
+ compilerOptions: {
42
+ // e.g. custom compiler options
43
+ }
44
+ }
45
+ }
46
+ })
47
+ ]
48
+ } satisfies UserConfig
49
+ ```
50
+
51
+ | Option | Type | Default | Description
52
+ | - | - | - | -
53
+ | `hydration` | `boolean` | `true` | When `true`, the server renders the page to HTML and the client hydrates it. When `false`, the client discards the server-rendered HTML on load and mounts a fresh tree — useful for highly interactive pages where paying the hydration-mismatch tax isn't worth it.
54
+ | `solid` | `Options` (from `vite-plugin-solid`) | `{}` | Passed through to the underlying `vite-plugin-solid` instance. Use this for custom Babel plugins or any other low-level Solid compiler setting.
55
+
56
+ ### 🪝 Hooks
57
+
58
+ #### `useData`
59
+ Access the data fetched by your `+data` functions directly inside your Vue components.
60
+
61
+ ```vue
62
+ <!-- /pages/+Page.vue -->
63
+ <script setup lang="ts">
64
+ import { useData } from 'vike-lite-vue'
65
+
66
+ type MyData = { title: string }
67
+
68
+ const [data, setData] = useData<MyData>()
69
+ </script>
70
+
71
+ <template>
72
+ <div>
73
+ <h1>{{ data.title }}</h1>
74
+ <button @click="setData(prev => ({ ...prev, title: 'Updated Title!' }))">
75
+ Update Data
76
+ </button>
77
+ </div>
78
+ </template>
79
+ ```
80
+
81
+ > 💡 **Note:** `data` is a `ComputedRef` — access it with `data.value` in `<script setup>`, but **without** `.value` directly in `<template>` (Vue automatically unwraps refs there). Like `vike-lite-solid` and `vike-lite-react`, `useData` returns a `[data, setData]` tuple so you can mutate the route data locally without an extra state manager.
82
+
83
+ #### `usePageContext`
84
+ Access the current page context, including URL parameters, original pathname, and route information.
85
+
86
+ ```vue
87
+ <!-- /pages/+Page.vue -->
88
+ <script setup lang="ts">
89
+ import { usePageContext } from 'vike-lite-vue'
90
+
91
+ const pageContext = usePageContext()
92
+ </script>
93
+
94
+ <template>
95
+ <p>Current Path: <strong>{{ pageContext.urlPathname }}</strong></p>
96
+ </template>
97
+ ```
98
+
99
+ > 💡 **Note:** `pageContext` here is a Vue `reactive()` object, not a ref — access its properties directly (`pageContext.urlPathname`), both in `<script setup>` and in `<template>`.
100
+
101
+ #### `useHydrated`
102
+ Detect whether the application has successfully hydrated on the client. Essential for wrapping client-only libraries (like chart tools or window-dependent logic) to avoid SSR hydration mismatches.
103
+
104
+ ```vue
105
+ <!-- /pages/+Page.vue -->
106
+ <script setup lang="ts">
107
+ import { useHydrated } from 'vike-lite-vue'
108
+ import ClientOnlyChart from './Chart.vue'
109
+
110
+ const hydrated = useHydrated()
111
+ </script>
112
+
113
+ <template>
114
+ <div>
115
+ <h1>Statistics</h1>
116
+ <ClientOnlyChart v-if="hydrated" />
117
+ <p v-else>Loading chart…</p>
118
+ </div>
119
+ </template>
120
+ ```
121
+
122
+ #### `useUrl`
123
+ ```vue
124
+ <!-- /pages/+Page.vue -->
125
+ <script setup lang="ts">
126
+ import { useUrl } from 'vike-lite-vue'
127
+
128
+ const url = useUrl()
129
+ </script>
130
+
131
+ <template>
132
+ <p>Current Query Parameter "myQueryParam": <strong>{{ url.searchParams.get('myQueryParam') }}</strong></p>
133
+ </template>
134
+ ```
135
+
136
+ > 💡 **Note:** Like `vike-lite-solid` and `vike-lite-react`, `vike-lite-vue` provides a dedicated `useUrl` composable — a granular alternative to parsing `pageContext.urlOriginal` manually.
137
+
138
+ ### Differences: `vike-vue` vs `vike-lite-vue`
139
+
140
+ | **Feature** | `vike-vue` | `vike-lite-vue` | **Why it matters**
141
+ | - | - | - | -
142
+ | **Reactivity Architecture** | _Single Source of Truth_ | _Separation of Concerns_ | `vike-lite-vue` keeps page data (`pageContext`) and the active UI (`view`: Page/Layout/Head) as two separate reactive atoms, so a data update doesn't force Vue to re-resolve which components are mounted, and vice versa.
143
+ | **Accessibility (A11y)** | _Not_ handled by default | _Automatic_ handled | After a client-side navigation, `vike-lite-vue` moves the focus to `#root`. This significantly improves UX for keyboard navigation and screen readers.
144
+ | `useData()` **Composable** | `getter` only | `[getter, setter]` | `vike-lite-vue` allows you to mutate the route data locally without needing other state managers.
145
+
146
+ ---
147
+
148
+ This project is licensed under the [MIT License](../../LICENSE).
@@ -0,0 +1,8 @@
1
+ import { VikeState } from "vike-lite/__internal/server";
2
+ //#region src/__internal/client/onRenderClient.d.ts
3
+ declare function onRenderClient(clientOptions: {
4
+ routes: VikeState['routes'];
5
+ errorRoute: VikeState['errorRoute'];
6
+ }): Promise<void>;
7
+ //#endregion
8
+ export { onRenderClient as default };
@@ -0,0 +1,283 @@
1
+ import { t as pageContextInjectionKey } from "../../globalContext-Dk48ds1e.mjs";
2
+ import { computed, createSSRApp, defineComponent, h, onMounted, onUnmounted, reactive, ref, watch } from "vue";
3
+ import { matchRoute } from "vike-lite/__internal/shared";
4
+ import { hydration } from "virtual:vike-lite/config";
5
+ //#region src/__internal/shared/stripBase.ts
6
+ function stripBase(pathname) {
7
+ const { BASE_URL } = import.meta.env;
8
+ if (BASE_URL === "/" || !pathname.startsWith(BASE_URL)) return pathname;
9
+ const stripped = pathname.slice(BASE_URL.length);
10
+ return stripped.startsWith("/") ? stripped : "/" + stripped;
11
+ }
12
+ //#endregion
13
+ //#region src/__internal/client/onRenderClient.ts
14
+ const RouterApp = defineComponent((props) => {
15
+ const pageContext = reactive({ ...props.initialContext });
16
+ const view = ref(props.initialView);
17
+ const currentUrl = ref(props.initialUrl);
18
+ const currentPathname = ref(props.initialContext.urlPathname);
19
+ const reloadTick = ref(0);
20
+ const shouldScrollToTop = { value: false };
21
+ const pendingContextOverride = { value: null };
22
+ const reloadResolvers = [];
23
+ let isFirstRun = true;
24
+ let abortController = null;
25
+ const matchedRoute = computed(() => matchRoute(currentPathname.value, props.routes));
26
+ function setPageContext(next) {
27
+ for (const key of Object.keys(pageContext)) delete pageContext[key];
28
+ Object.assign(pageContext, next);
29
+ }
30
+ async function loadRoute() {
31
+ abortController?.abort();
32
+ const controller = new AbortController();
33
+ abortController = controller;
34
+ const signal = controller.signal;
35
+ const pathname = currentPathname.value;
36
+ const urlFull = currentUrl.value;
37
+ const isReload = reloadTick.value > 0;
38
+ const matched = matchedRoute.value;
39
+ const contextOverride = pendingContextOverride.value;
40
+ pendingContextOverride.value = null;
41
+ function finalizeNavigation() {
42
+ if (shouldScrollToTop.value) {
43
+ window.scrollTo(0, 0);
44
+ shouldScrollToTop.value = false;
45
+ } else if (globalThis.location.hash) requestAnimationFrame(() => {
46
+ try {
47
+ document.querySelector(decodeURIComponent(globalThis.location.hash))?.scrollIntoView();
48
+ } catch {}
49
+ });
50
+ }
51
+ const renderErrorPage = async (is404, message) => {
52
+ if (!props.errorRoute) return;
53
+ const [ErrorPageMod, ErrorLayoutMod, ErrorHeadMod] = await Promise.all([
54
+ props.errorRoute.Page(),
55
+ props.errorRoute.Layout?.() ?? null,
56
+ props.errorRoute.Head?.() ?? null
57
+ ]);
58
+ if (signal.aborted) return;
59
+ setPageContext({
60
+ urlOriginal: urlFull,
61
+ urlPathname: pathname,
62
+ routeParams: {},
63
+ is404,
64
+ is500: !is404,
65
+ errorMessage: message
66
+ });
67
+ view.value = {
68
+ Page: ErrorPageMod.Page ?? ErrorPageMod.default,
69
+ Layout: ErrorLayoutMod?.Layout ?? ErrorLayoutMod?.default ?? null,
70
+ Head: ErrorHeadMod?.Head ?? ErrorHeadMod?.default ?? null
71
+ };
72
+ document.title = is404 ? "Not Found" : "Server Error";
73
+ finalizeNavigation();
74
+ };
75
+ if (!matched) return renderErrorPage(true);
76
+ const { route, routeParams } = matched;
77
+ try {
78
+ const urlObj = new URL(urlFull);
79
+ const jsonTarget = pathname === "/" ? "/index" : pathname;
80
+ const { BASE_URL } = import.meta.env;
81
+ const jsonUrl = `${BASE_URL.endsWith("/") ? BASE_URL.slice(0, -1) : BASE_URL}${jsonTarget}.pageContext.json${urlObj.search}`;
82
+ let ctx = null;
83
+ if (route.data || route.title) {
84
+ const res = await fetch(jsonUrl, {
85
+ signal,
86
+ cache: isReload ? "no-cache" : "default"
87
+ });
88
+ const contentType = res.headers.get("content-type") ?? "";
89
+ if (!contentType.includes("application/json")) throw new Error(`Expected JSON but got "${contentType}" for ${jsonUrl}. Check your proxy/CDN configuration.`);
90
+ ctx = await res.json();
91
+ }
92
+ if (signal.aborted) return;
93
+ if (ctx?._redirect) {
94
+ const urlObjRedirect = new URL(ctx._redirect, globalThis.location.origin);
95
+ if (urlObjRedirect.origin !== globalThis.location.origin) {
96
+ globalThis.location.assign(ctx._redirect);
97
+ return;
98
+ }
99
+ globalThis.history.pushState({ triggeredBy: "vike-lite" }, "", ctx._redirect);
100
+ shouldScrollToTop.value = true;
101
+ currentUrl.value = urlObjRedirect.href;
102
+ currentPathname.value = stripBase(urlObjRedirect.pathname);
103
+ return;
104
+ }
105
+ if (ctx && (ctx.is404 || ctx.is500 || ctx.isError)) return renderErrorPage(ctx.is404, ctx.reason || "Server Error");
106
+ const [PageMod, LayoutMod, HeadMod] = await Promise.all([
107
+ route.Page(),
108
+ route.Layout?.() ?? null,
109
+ route.Head?.() ?? null
110
+ ]);
111
+ if (signal.aborted) return;
112
+ setPageContext({
113
+ routeParams,
114
+ urlOriginal: urlObj.href,
115
+ urlPathname: pathname,
116
+ search: urlObj.search,
117
+ ...ctx?.data && { data: ctx.data },
118
+ ...ctx?.title && { title: ctx.title },
119
+ ...contextOverride
120
+ });
121
+ view.value = {
122
+ Page: PageMod.Page ?? PageMod.default,
123
+ Layout: LayoutMod?.Layout ?? LayoutMod?.default ?? null,
124
+ Head: HeadMod?.Head ?? HeadMod?.default ?? null
125
+ };
126
+ if (ctx?.title) document.title = ctx.title;
127
+ requestAnimationFrame(() => {
128
+ if (globalThis.location.hash) return;
129
+ document.querySelector("#root")?.focus({ preventScroll: true });
130
+ });
131
+ finalizeNavigation();
132
+ } catch (error) {
133
+ if (error.name === "AbortError") return;
134
+ const message = error.message || "";
135
+ if (/dynamically imported module|importing a module script failed/i.test(message)) {
136
+ const GUARD_KEY = "vike-lite:reload-guard";
137
+ const last = Number(sessionStorage.getItem(GUARD_KEY) ?? 0);
138
+ if (Date.now() - last > 1e4) {
139
+ sessionStorage.setItem(GUARD_KEY, String(Date.now()));
140
+ console.warn("App update detected, forcing reload…");
141
+ globalThis.location.assign(urlFull);
142
+ return;
143
+ }
144
+ }
145
+ console.error("Router Error:", error);
146
+ renderErrorPage(false, message);
147
+ } finally {
148
+ if (!signal.aborted) {
149
+ for (const resolve of reloadResolvers) resolve();
150
+ reloadResolvers.length = 0;
151
+ }
152
+ }
153
+ }
154
+ onMounted(() => {
155
+ const handleLinkClick = (e) => {
156
+ const target = e.target.closest("a");
157
+ if (!target?.href || target.target === "_blank" || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
158
+ const url = new URL(target.href);
159
+ if (url.origin !== globalThis.location.origin) return;
160
+ if (url.pathname === globalThis.location.pathname && url.search === globalThis.location.search) return;
161
+ e.preventDefault();
162
+ globalThis.history.pushState({ triggeredBy: "vike-lite" }, "", url.href);
163
+ if (!url.hash) shouldScrollToTop.value = true;
164
+ currentUrl.value = url.href;
165
+ currentPathname.value = stripBase(url.pathname);
166
+ };
167
+ const prefetchedModules = /* @__PURE__ */ new Set();
168
+ function prefetchRoute(route) {
169
+ const modules = [
170
+ [route.page, route.Page],
171
+ [route.layout, route.Layout],
172
+ [route.head, route.Head]
173
+ ];
174
+ for (const [key, loader] of modules) {
175
+ if (!key || !loader || prefetchedModules.has(key)) continue;
176
+ prefetchedModules.add(key);
177
+ loader().catch(() => {
178
+ prefetchedModules.delete(key);
179
+ });
180
+ }
181
+ }
182
+ const handleLinkPrefetch = (e) => {
183
+ const target = e.target.closest("a");
184
+ if (!target?.href || target.target && target.target !== "_self" || target.hasAttribute("download")) return;
185
+ const url = new URL(target.href);
186
+ if (url.origin !== globalThis.location.origin) return;
187
+ if (url.pathname === globalThis.location.pathname && url.search === globalThis.location.search) return;
188
+ const matched = matchRoute(stripBase(url.pathname), props.routes);
189
+ if (matched) prefetchRoute(matched.route);
190
+ };
191
+ const handlePopState = () => {
192
+ currentUrl.value = globalThis.location.href;
193
+ currentPathname.value = stripBase(globalThis.location.pathname);
194
+ };
195
+ const handleProgrammaticNavigate = (e) => {
196
+ const detail = e.detail || {};
197
+ if (!detail.keepScrollPosition) shouldScrollToTop.value = true;
198
+ if (detail.pageContext) pendingContextOverride.value = detail.pageContext;
199
+ currentUrl.value = globalThis.location.href;
200
+ currentPathname.value = stripBase(globalThis.location.pathname);
201
+ };
202
+ const handleProgrammaticReload = (e) => {
203
+ const resolve = e.detail?.resolve;
204
+ if (resolve) reloadResolvers.push(resolve);
205
+ reloadTick.value++;
206
+ };
207
+ document.addEventListener("click", handleLinkClick);
208
+ document.addEventListener("pointerenter", handleLinkPrefetch, { capture: true });
209
+ document.addEventListener("focusin", handleLinkPrefetch);
210
+ globalThis.addEventListener("popstate", handlePopState);
211
+ globalThis.addEventListener("vike-navigate", handleProgrammaticNavigate);
212
+ globalThis.addEventListener("vike-reload", handleProgrammaticReload);
213
+ onUnmounted(() => {
214
+ document.removeEventListener("click", handleLinkClick);
215
+ document.removeEventListener("pointerenter", handleLinkPrefetch, { capture: true });
216
+ document.removeEventListener("focusin", handleLinkPrefetch);
217
+ globalThis.removeEventListener("popstate", handlePopState);
218
+ globalThis.removeEventListener("vike-navigate", handleProgrammaticNavigate);
219
+ globalThis.removeEventListener("vike-reload", handleProgrammaticReload);
220
+ });
221
+ });
222
+ watch([
223
+ currentPathname,
224
+ currentUrl,
225
+ reloadTick
226
+ ], () => {
227
+ const pathname = currentPathname.value;
228
+ if (isFirstRun && reloadTick.value === 0 && globalThis.__PAGE_CONTEXT__?.urlPathname === pathname) {
229
+ isFirstRun = false;
230
+ globalThis.__PAGE_CONTEXT__.urlPathname = void 0;
231
+ return;
232
+ }
233
+ isFirstRun = false;
234
+ loadRoute();
235
+ });
236
+ return () => {
237
+ const { Page, Layout } = view.value;
238
+ if (!Page) return null;
239
+ return h("div", [Layout ? h(Layout, null, { default: () => h(Page) }) : h(Page)]);
240
+ };
241
+ }, { props: [
242
+ "routes",
243
+ "errorRoute",
244
+ "initialView",
245
+ "initialContext",
246
+ "initialUrl"
247
+ ] });
248
+ async function onRenderClient(clientOptions) {
249
+ const container = document.querySelector("#root");
250
+ const initialContext = globalThis.__PAGE_CONTEXT__ ?? {};
251
+ const isHydration = hydration && !!globalThis.__PAGE_CONTEXT__;
252
+ let initialView = {
253
+ Page: null,
254
+ Layout: null,
255
+ Head: null
256
+ };
257
+ if (isHydration) {
258
+ const matched = matchRoute(initialContext.urlPathname ?? globalThis.location.pathname, clientOptions.routes);
259
+ if (matched) {
260
+ const [PageMod, LayoutMod, HeadMod] = await Promise.all([
261
+ matched.route.Page(),
262
+ matched.route.Layout?.() ?? null,
263
+ matched.route.Head?.() ?? null
264
+ ]);
265
+ initialView = {
266
+ Page: PageMod.Page ?? PageMod.default,
267
+ Layout: LayoutMod?.Layout ?? LayoutMod?.default ?? null,
268
+ Head: HeadMod?.Head ?? HeadMod?.default ?? null
269
+ };
270
+ }
271
+ }
272
+ const app = createSSRApp(RouterApp, {
273
+ ...clientOptions,
274
+ initialView,
275
+ initialContext,
276
+ initialUrl: globalThis.location.href
277
+ });
278
+ app.provide(pageContextInjectionKey, { pageContext: reactive(initialContext) });
279
+ if (!isHydration) container.replaceChildren();
280
+ app.mount(container);
281
+ }
282
+ //#endregion
283
+ export { onRenderClient as default };
@@ -0,0 +1,11 @@
1
+ import { Component } from "vue";
2
+ import { RenderContext } from "vike-lite/__internal/shared";
3
+ //#region src/__internal/server/onRenderHtml.d.ts
4
+ interface VueRenderContext extends RenderContext {
5
+ Page: Component;
6
+ Head?: Component;
7
+ Layout?: Component;
8
+ }
9
+ declare function onRenderHtml({ pageContext, Page, Head, Layout, pageTitleTag, serializedContext, assets }: VueRenderContext): Promise<string>;
10
+ //#endregion
11
+ export { VueRenderContext, onRenderHtml };
@@ -0,0 +1,34 @@
1
+ import { t as pageContextInjectionKey } from "../../globalContext-Dk48ds1e.mjs";
2
+ import { createSSRApp, h } from "vue";
3
+ import { renderToString } from "vue/server-renderer";
4
+ //#region src/__internal/server/onRenderHtml.ts
5
+ async function onRenderHtml({ pageContext, Page, Head, Layout, pageTitleTag, serializedContext, assets }) {
6
+ const { cssLinks, jsPreloads, entryClient } = assets;
7
+ let headHtml = "";
8
+ if (Head) {
9
+ const headApp = createSSRApp({ render: () => h(Head) });
10
+ headApp.provide(pageContextInjectionKey, { pageContext });
11
+ headHtml = await renderToString(headApp);
12
+ }
13
+ const app = createSSRApp({ render: () => Layout ? h(Layout, null, { default: () => h(Page) }) : h(Page) });
14
+ app.provide(pageContextInjectionKey, { pageContext });
15
+ const appHtml = await renderToString(app);
16
+ return `<!DOCTYPE html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8">
20
+ <meta name="viewport" content="width=device-width,initial-scale=1">
21
+ ${pageTitleTag}
22
+ ${cssLinks}
23
+ ${jsPreloads}
24
+ ${headHtml}
25
+ <script>window.__PAGE_CONTEXT__=${serializedContext}<\/script>
26
+ </head>
27
+ <body>
28
+ <div id="root">${appHtml}</div>
29
+ <script type="module" src="${entryClient}"><\/script>
30
+ </body>
31
+ </html>`;
32
+ }
33
+ //#endregion
34
+ export { onRenderHtml };
@@ -0,0 +1,7 @@
1
+ //#region src/hooks/globalContext.ts
2
+ const KEY = "_vike_lite_vue_context";
3
+ const g = globalThis;
4
+ if (!Object.hasOwn(g, KEY)) g[KEY] = Symbol("vike-lite-vue:pageContext");
5
+ const pageContextInjectionKey = g[KEY];
6
+ //#endregion
7
+ export { pageContextInjectionKey as t };
@@ -0,0 +1,15 @@
1
+ import { ComputedRef, Ref } from "vue";
2
+ import { PageContext } from "vike-lite";
3
+ //#region src/hooks/useData.d.ts
4
+ declare function useData<Data = unknown>(): [ComputedRef<Data>, (updater: Data | ((prev: Data) => Data)) => void];
5
+ //#endregion
6
+ //#region src/hooks/useHydrated.d.ts
7
+ declare function useHydrated(): Ref<boolean>;
8
+ //#endregion
9
+ //#region src/hooks/usePageContext.d.ts
10
+ declare function usePageContext(): PageContext;
11
+ //#endregion
12
+ //#region src/hooks/useUrl.d.ts
13
+ declare function useUrl(): ComputedRef<URL>;
14
+ //#endregion
15
+ export { useData, useHydrated, usePageContext, useUrl };
package/dist/index.mjs ADDED
@@ -0,0 +1,37 @@
1
+ import { t as pageContextInjectionKey } from "./globalContext-Dk48ds1e.mjs";
2
+ import { computed, inject, onMounted, ref } from "vue";
3
+ //#region src/hooks/useData.ts
4
+ function useData() {
5
+ const ctx = inject(pageContextInjectionKey);
6
+ if (!ctx) throw new Error("useData() must be called inside a page rendered by vike-lite-vue");
7
+ const data = computed(() => ctx.pageContext.data);
8
+ const setData = (updater) => {
9
+ const next = typeof updater === "function" ? updater(ctx.pageContext.data) : updater;
10
+ ctx.pageContext.data = next;
11
+ };
12
+ return [data, setData];
13
+ }
14
+ //#endregion
15
+ //#region src/hooks/useHydrated.ts
16
+ function useHydrated() {
17
+ const hydrated = ref(false);
18
+ onMounted(() => {
19
+ hydrated.value = true;
20
+ });
21
+ return hydrated;
22
+ }
23
+ //#endregion
24
+ //#region src/hooks/usePageContext.ts
25
+ function usePageContext() {
26
+ const ctx = inject(pageContextInjectionKey);
27
+ if (!ctx) throw new Error("usePageContext() must be called inside a page rendered by vike-lite-vue");
28
+ return ctx.pageContext;
29
+ }
30
+ //#endregion
31
+ //#region src/hooks/useUrl.ts
32
+ function useUrl() {
33
+ const pageContext = usePageContext();
34
+ return computed(() => new URL(pageContext.urlOriginal));
35
+ }
36
+ //#endregion
37
+ export { useData, useHydrated, usePageContext, useUrl };
@@ -0,0 +1,17 @@
1
+ import { Options } from "@vitejs/plugin-vue";
2
+ import { PluginOption } from "vite";
3
+ //#region src/vite.d.ts
4
+ declare function vikeLiteVue({ hydration, vue: vueUserOptions }?: {
5
+ /**
6
+ * true: enable Vue Hydration (better UX)
7
+ * false: destroy the SSR DOM and recreate the app (Client Takeover, no hydration mismatch)
8
+ * @default true
9
+ */
10
+ hydration?: boolean;
11
+ /**
12
+ * Advanced options passed directly to @vitejs/plugin-vue
13
+ */
14
+ vue?: Partial<Options>;
15
+ }): PluginOption[];
16
+ //#endregion
17
+ export { vikeLiteVue as default };
package/dist/vite.mjs ADDED
@@ -0,0 +1,36 @@
1
+ import vuePlugin from "@vitejs/plugin-vue";
2
+ import { mergeConfig } from "vite";
3
+ //#region src/vite.ts
4
+ function vikeLiteVue({ hydration = true, vue: vueUserOptions = {} } = {}) {
5
+ const virtualConfigId = "virtual:vike-lite/config";
6
+ const virtualClientId = "virtual:vike-lite/client";
7
+ const virtualServerId = "virtual:vike-lite/server";
8
+ const resolvedVirtualConfigId = "\0virtual:vike-lite/config";
9
+ const resolvedVirtualClientId = "\0virtual:vike-lite/client";
10
+ const resolvedVirtualServerId = "\0virtual:vike-lite/server";
11
+ return [vuePlugin(mergeConfig({ ssr: true }, vueUserOptions)), {
12
+ name: "vike-lite-vue",
13
+ enforce: "pre",
14
+ config() {
15
+ return {
16
+ optimizeDeps: {
17
+ include: ["vue"],
18
+ exclude: ["vike-lite-vue"]
19
+ },
20
+ ssr: { noExternal: ["vike-lite-vue"] }
21
+ };
22
+ },
23
+ resolveId(id) {
24
+ if (id === virtualConfigId) return resolvedVirtualConfigId;
25
+ if (id === virtualClientId) return resolvedVirtualClientId;
26
+ if (id === virtualServerId) return resolvedVirtualServerId;
27
+ },
28
+ load(id) {
29
+ if (id === resolvedVirtualConfigId) return `export const hydration=${hydration};`;
30
+ if (id === resolvedVirtualClientId) return `export const onRenderClient=()=>import('vike-lite-vue/__internal/client/onRenderClient');`;
31
+ if (id === resolvedVirtualServerId) return `export{onRenderHtml}from'vike-lite-vue/__internal/server/onRenderHtml';`;
32
+ }
33
+ }];
34
+ }
35
+ //#endregion
36
+ export { vikeLiteVue as default };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "vike-lite-vue",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ },
13
+ "./vite": {
14
+ "types": "./dist/vite.d.mts",
15
+ "import": "./dist/vite.mjs"
16
+ },
17
+ "./__internal/client/onRenderClient": {
18
+ "types": "./dist/__internal/client/onRenderClient.d.mts",
19
+ "import": "./dist/__internal/client/onRenderClient.mjs"
20
+ },
21
+ "./__internal/server/onRenderHtml": {
22
+ "types": "./dist/__internal/server/onRenderHtml.d.mts",
23
+ "import": "./dist/__internal/server/onRenderHtml.mjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "tsc": "vue-tsc --noEmit",
31
+ "build": "tsdown"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^26.1.1",
35
+ "@vitejs/plugin-vue": "^6.0.7",
36
+ "@vue/compiler-sfc": "^3.5.39",
37
+ "@vue/server-renderer": "^3.5.39",
38
+ "tsdown": "^0.22.5",
39
+ "typescript": "^6.0.3",
40
+ "unplugin-vue": "^7.2.0",
41
+ "vike-lite": "1.13.2",
42
+ "vite": "^8.1.4",
43
+ "vue": "^3.5.39",
44
+ "vue-tsc": "^3.3.7"
45
+ },
46
+ "peerDependencies": {
47
+ "@vitejs/plugin-vue": ">=6",
48
+ "vike-lite": "*",
49
+ "vite": ">=8",
50
+ "vue": "*"
51
+ },
52
+ "engines": {
53
+ "node": "^20.19.0 || >=22.12.0"
54
+ }
55
+ }