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 +148 -0
- package/dist/__internal/client/onRenderClient.d.mts +8 -0
- package/dist/__internal/client/onRenderClient.mjs +283 -0
- package/dist/__internal/server/onRenderHtml.d.mts +11 -0
- package/dist/__internal/server/onRenderHtml.mjs +34 -0
- package/dist/globalContext-Dk48ds1e.mjs +7 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +37 -0
- package/dist/vite.d.mts +17 -0
- package/dist/vite.mjs +36 -0
- package/package.json +55 -0
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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/vite.d.mts
ADDED
|
@@ -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
|
+
}
|