vike-lite 1.15.20 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__internal/client.d.mts +12 -1
- package/dist/__internal/client.mjs +15 -1
- package/dist/__internal/server.d.mts +24 -1
- package/dist/__internal/server.mjs +58 -4
- package/dist/__internal/vite.d.mts +9 -1
- package/dist/__internal/vite.mjs +2 -2
- package/dist/vite.mjs +63 -17
- package/package.json +2 -2
|
@@ -83,6 +83,17 @@ declare function buildInitialClientContext<T extends object>(rawContext: T | und
|
|
|
83
83
|
isClientSide: true;
|
|
84
84
|
isHydration: boolean;
|
|
85
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* Build the pageContext object for a completed client-side navigation. Centralized
|
|
88
|
+
* here (rather than each adapter constructing its own object literal) so the
|
|
89
|
+
* required `isClientSide`/`isHydration` flags can never be silently omitted —
|
|
90
|
+
* every adapter previously had to remember to add them by hand, and more than
|
|
91
|
+
* one forgot, silently dropping the flags from `pageContext` on every navigation.
|
|
92
|
+
*/
|
|
93
|
+
declare function buildNavigationPageContext<T extends object>(fields: T): T & {
|
|
94
|
+
isClientSide: true;
|
|
95
|
+
isHydration: false;
|
|
96
|
+
};
|
|
86
97
|
interface HydrationInitialContext {
|
|
87
98
|
urlPathname?: string;
|
|
88
99
|
is404?: boolean;
|
|
@@ -107,4 +118,4 @@ declare function resolveHydrationView<TComponent = unknown>(initialContext: Hydr
|
|
|
107
118
|
*/
|
|
108
119
|
declare function consumeMatchingInitialContext(pathname: string): boolean;
|
|
109
120
|
//#endregion
|
|
110
|
-
export { PageContextJson, ViewComponents, buildInitialClientContext, buildPageContextJsonUrl, consumeMatchingInitialContext, createLinkClickHandler, createLinkPrefetchHandler, createRoutePrefetcher, fetchPageContextJson, finalizeNavigation, loadViewModules, resolveHydrationView, tryRecoverFromStaleModuleGraph };
|
|
121
|
+
export { PageContextJson, ViewComponents, buildInitialClientContext, buildNavigationPageContext, buildPageContextJsonUrl, consumeMatchingInitialContext, createLinkClickHandler, createLinkPrefetchHandler, createRoutePrefetcher, fetchPageContextJson, finalizeNavigation, loadViewModules, resolveHydrationView, tryRecoverFromStaleModuleGraph };
|
|
@@ -125,6 +125,20 @@ function buildInitialClientContext(rawContext, isHydration) {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
/**
|
|
128
|
+
* Build the pageContext object for a completed client-side navigation. Centralized
|
|
129
|
+
* here (rather than each adapter constructing its own object literal) so the
|
|
130
|
+
* required `isClientSide`/`isHydration` flags can never be silently omitted —
|
|
131
|
+
* every adapter previously had to remember to add them by hand, and more than
|
|
132
|
+
* one forgot, silently dropping the flags from `pageContext` on every navigation.
|
|
133
|
+
*/
|
|
134
|
+
function buildNavigationPageContext(fields) {
|
|
135
|
+
return {
|
|
136
|
+
...fields,
|
|
137
|
+
isClientSide: true,
|
|
138
|
+
isHydration: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
128
142
|
* Resolve the Page/Layout/Head modules to hydrate with, matching what the server
|
|
129
143
|
* actually rendered into the HTML: the error route's modules when the server
|
|
130
144
|
* reported a 404/500/error, otherwise the modules of the route matching the
|
|
@@ -156,4 +170,4 @@ function consumeMatchingInitialContext(pathname) {
|
|
|
156
170
|
return true;
|
|
157
171
|
}
|
|
158
172
|
//#endregion
|
|
159
|
-
export { buildInitialClientContext, buildPageContextJsonUrl, consumeMatchingInitialContext, createLinkClickHandler, createLinkPrefetchHandler, createRoutePrefetcher, fetchPageContextJson, finalizeNavigation, loadViewModules, resolveHydrationView, tryRecoverFromStaleModuleGraph };
|
|
173
|
+
export { buildInitialClientContext, buildNavigationPageContext, buildPageContextJsonUrl, consumeMatchingInitialContext, createLinkClickHandler, createLinkPrefetchHandler, createRoutePrefetcher, fetchPageContextJson, finalizeNavigation, loadViewModules, resolveHydrationView, tryRecoverFromStaleModuleGraph };
|
|
@@ -25,5 +25,28 @@ interface HtmlShellParams {
|
|
|
25
25
|
* React/Vue/Solid, as it did before this was factored out.
|
|
26
26
|
*/
|
|
27
27
|
declare function renderHtmlShell({ pageTitleTag, cssLinks, jsPreloads, headHtml, appHtml, serializedContext, entryClient, nonce, extraHeadHtml }: HtmlShellParams): string;
|
|
28
|
+
interface HtmlShellStreamParams extends Omit<HtmlShellParams, 'appHtml'> {
|
|
29
|
+
/**
|
|
30
|
+
* The app markup, as a stream of `string` or `Uint8Array` chunks (whatever the
|
|
31
|
+
* UI framework's streaming renderer produces). Framework adapters build this by
|
|
32
|
+
* calling their framework's own Web Streams-based SSR API — e.g. React's
|
|
33
|
+
* `renderToReadableStream` (from `react-dom/server.edge`) or Vue's
|
|
34
|
+
* `renderToWebStream` (from `vue/server-renderer`).
|
|
35
|
+
*/
|
|
36
|
+
appStream: ReadableStream<string | Uint8Array>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Same document shell as {@link renderHtmlShell}, but returns a standard
|
|
40
|
+
* `ReadableStream<Uint8Array>` instead of a fully-buffered `string`: the shell's
|
|
41
|
+
* opening tags are enqueued immediately, the framework's app stream is piped
|
|
42
|
+
* through chunk by chunk as it becomes available, and the closing tags are
|
|
43
|
+
* enqueued once the app stream ends.
|
|
44
|
+
*
|
|
45
|
+
* Built entirely on the platform-native `ReadableStream`/`TextEncoder` globals
|
|
46
|
+
* (no Node-specific stream types), so the exact same code streams correctly on
|
|
47
|
+
* Node.js (18+), Deno, Bun, and Edge runtimes (Cloudflare Workers, Vercel Edge,
|
|
48
|
+
* etc.) — anywhere a `Response` with a streamed body can be returned.
|
|
49
|
+
*/
|
|
50
|
+
declare function renderHtmlShellStream({ appStream, ...shellParams }: HtmlShellStreamParams): ReadableStream<Uint8Array>;
|
|
28
51
|
//#endregion
|
|
29
|
-
export { VikeState, renderHtmlShell, setVikeState, store };
|
|
52
|
+
export { VikeState, renderHtmlShell, renderHtmlShellStream, setVikeState, store };
|
|
@@ -8,8 +8,23 @@ import { n as store, t as setVikeState } from "../store-BCs8gL_o.mjs";
|
|
|
8
8
|
* React/Vue/Solid, as it did before this was factored out.
|
|
9
9
|
*/
|
|
10
10
|
function renderHtmlShell({ pageTitleTag, cssLinks, jsPreloads, headHtml, appHtml, serializedContext, entryClient, nonce, extraHeadHtml = "" }) {
|
|
11
|
+
const { start, end } = htmlShellParts({
|
|
12
|
+
pageTitleTag,
|
|
13
|
+
cssLinks,
|
|
14
|
+
jsPreloads,
|
|
15
|
+
headHtml,
|
|
16
|
+
serializedContext,
|
|
17
|
+
entryClient,
|
|
18
|
+
nonce,
|
|
19
|
+
extraHeadHtml
|
|
20
|
+
});
|
|
21
|
+
return `${start}${appHtml}${end}`;
|
|
22
|
+
}
|
|
23
|
+
/** Splits the shell markup around the app content so it can be streamed. */
|
|
24
|
+
function htmlShellParts({ pageTitleTag, cssLinks, jsPreloads, headHtml, serializedContext, entryClient, nonce, extraHeadHtml = "" }) {
|
|
11
25
|
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
12
|
-
return
|
|
26
|
+
return {
|
|
27
|
+
start: `<!DOCTYPE html>
|
|
13
28
|
<html lang="en">
|
|
14
29
|
<head>
|
|
15
30
|
<meta charset="utf-8">
|
|
@@ -21,10 +36,49 @@ ${headHtml}
|
|
|
21
36
|
${extraHeadHtml ? `${extraHeadHtml}\n` : ""}<script${nonceAttr}>window.__PAGE_CONTEXT__=${serializedContext}<\/script>
|
|
22
37
|
</head>
|
|
23
38
|
<body>
|
|
24
|
-
<div id="root" tabindex="-1"
|
|
39
|
+
<div id="root" tabindex="-1">`,
|
|
40
|
+
end: `</div>
|
|
25
41
|
<script type="module" src="${entryClient}"${nonceAttr}><\/script>
|
|
26
42
|
</body>
|
|
27
|
-
</html
|
|
43
|
+
</html>`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Same document shell as {@link renderHtmlShell}, but returns a standard
|
|
48
|
+
* `ReadableStream<Uint8Array>` instead of a fully-buffered `string`: the shell's
|
|
49
|
+
* opening tags are enqueued immediately, the framework's app stream is piped
|
|
50
|
+
* through chunk by chunk as it becomes available, and the closing tags are
|
|
51
|
+
* enqueued once the app stream ends.
|
|
52
|
+
*
|
|
53
|
+
* Built entirely on the platform-native `ReadableStream`/`TextEncoder` globals
|
|
54
|
+
* (no Node-specific stream types), so the exact same code streams correctly on
|
|
55
|
+
* Node.js (18+), Deno, Bun, and Edge runtimes (Cloudflare Workers, Vercel Edge,
|
|
56
|
+
* etc.) — anywhere a `Response` with a streamed body can be returned.
|
|
57
|
+
*/
|
|
58
|
+
function renderHtmlShellStream({ appStream, ...shellParams }) {
|
|
59
|
+
const { start, end } = htmlShellParts(shellParams);
|
|
60
|
+
const encoder = new TextEncoder();
|
|
61
|
+
return new ReadableStream({
|
|
62
|
+
async start(controller) {
|
|
63
|
+
controller.enqueue(encoder.encode(start));
|
|
64
|
+
const reader = appStream.getReader();
|
|
65
|
+
try {
|
|
66
|
+
while (true) {
|
|
67
|
+
const { done, value } = await reader.read();
|
|
68
|
+
if (done) break;
|
|
69
|
+
controller.enqueue(typeof value === "string" ? encoder.encode(value) : value);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
controller.error(error);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
controller.enqueue(encoder.encode(end));
|
|
76
|
+
controller.close();
|
|
77
|
+
},
|
|
78
|
+
async cancel(reason) {
|
|
79
|
+
await appStream.cancel?.(reason);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
28
82
|
}
|
|
29
83
|
//#endregion
|
|
30
|
-
export { renderHtmlShell, setVikeState, store };
|
|
84
|
+
export { renderHtmlShell, renderHtmlShellStream, setVikeState, store };
|
|
@@ -27,13 +27,21 @@ declare function createDepsConfigPlugin({ packageName, optimizeDepsInclude }: {
|
|
|
27
27
|
*
|
|
28
28
|
* This centralizes logic that is otherwise identical across every `vike-lite-*` package.
|
|
29
29
|
*/
|
|
30
|
-
declare function createFrameworkAdapterPlugin({ packageName, hydration, wrapServerHydration }: {
|
|
30
|
+
declare function createFrameworkAdapterPlugin({ packageName, hydration, wrapServerHydration, streaming }: {
|
|
31
31
|
/** The framework adapter's package name, e.g. 'vike-lite-react'. */
|
|
32
32
|
packageName: string;
|
|
33
33
|
/** @default true */
|
|
34
34
|
hydration?: boolean;
|
|
35
35
|
/** @default true */
|
|
36
36
|
wrapServerHydration?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Stream the server-rendered app markup via the framework's Web Streams-based
|
|
39
|
+
* SSR API instead of buffering it into a single string. Threaded through to
|
|
40
|
+
* `onRenderHtml` the same way `hydration` is. Ignored by adapters whose
|
|
41
|
+
* `onRenderHtml` doesn't accept a `streaming` option.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
streaming?: boolean;
|
|
37
45
|
}): Plugin;
|
|
38
46
|
//#endregion
|
|
39
47
|
export { createDepsConfigPlugin, createFrameworkAdapterPlugin };
|
package/dist/__internal/vite.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function createDepsConfigPlugin({ packageName, optimizeDepsInclude }) {
|
|
|
35
35
|
*
|
|
36
36
|
* This centralizes logic that is otherwise identical across every `vike-lite-*` package.
|
|
37
37
|
*/
|
|
38
|
-
function createFrameworkAdapterPlugin({ packageName, hydration = true, wrapServerHydration = true }) {
|
|
38
|
+
function createFrameworkAdapterPlugin({ packageName, hydration = true, wrapServerHydration = true, streaming = false }) {
|
|
39
39
|
const virtualClientId = "virtual:vike-lite/client";
|
|
40
40
|
const virtualServerId = "virtual:vike-lite/server";
|
|
41
41
|
const resolvedVirtualClientId = "\0virtual:vike-lite/client";
|
|
@@ -49,7 +49,7 @@ function createFrameworkAdapterPlugin({ packageName, hydration = true, wrapServe
|
|
|
49
49
|
},
|
|
50
50
|
load(id) {
|
|
51
51
|
if (id === resolvedVirtualClientId) return `export const onRenderClient=async(options)=>(await import("${packageName}/__internal/client/onRenderClient")).onRenderClient({...options,hydration:${hydration}});`;
|
|
52
|
-
if (id === resolvedVirtualServerId) return
|
|
52
|
+
if (id === resolvedVirtualServerId) return `import { onRenderHtml as _onRenderHtml } from '${packageName}/__internal/server/onRenderHtml';export const onRenderHtml = (ctx) => _onRenderHtml({ ...ctx, ${wrapServerHydration ? `hydration:${hydration},streaming:${streaming}` : `streaming:${streaming}`} });`;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
}
|
package/dist/vite.mjs
CHANGED
|
@@ -377,37 +377,83 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
|
|
|
377
377
|
method: req.method,
|
|
378
378
|
headers
|
|
379
379
|
};
|
|
380
|
-
if (req.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const response = await app.fetch(new Request(`http://${
|
|
380
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
381
|
+
requestInit.body = Readable.toWeb(req);
|
|
382
|
+
requestInit.duplex = "half";
|
|
383
|
+
}
|
|
384
|
+
if (req.url.startsWith(apiPrefix)) server.config.logger.info(`⚡ API: ${req.method} ${req.url}`, { timestamp: true });
|
|
385
|
+
else if (req.url.endsWith(".pageContext.json")) server.config.logger.info(`🔄 SPA Navigation: ${req.url}`, { timestamp: true });
|
|
386
|
+
const host = req.headers.host || "localhost";
|
|
387
|
+
const response = await app.fetch(new Request(`http://${host}${req.url}`, requestInit));
|
|
388
388
|
res.statusCode = response.status;
|
|
389
|
+
for (const [key, value] of response.headers) if (key.toLowerCase() !== "set-cookie") res.setHeader(key, value);
|
|
390
|
+
const cookies = response.headers.getSetCookie();
|
|
391
|
+
if (cookies.length > 0) res.setHeader("Set-Cookie", cookies);
|
|
392
|
+
async function safeCancelBody(body) {
|
|
393
|
+
if (!body || body.locked) return;
|
|
394
|
+
try {
|
|
395
|
+
await body.cancel();
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
389
398
|
if (response.headers.get("content-type")?.includes("text/html")) {
|
|
399
|
+
res.removeHeader("content-length");
|
|
390
400
|
server.config.logger.info(`📄 Page: ${req.url}`, { timestamp: true });
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
res.
|
|
401
|
+
if (req.method === "HEAD" || !response.body) {
|
|
402
|
+
await safeCancelBody(response.body);
|
|
403
|
+
return res.end();
|
|
404
|
+
}
|
|
405
|
+
if (res.destroyed || res.closed) {
|
|
406
|
+
await safeCancelBody(response.body);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let headBuffered = "";
|
|
410
|
+
let headInjected = false;
|
|
411
|
+
const decoder = new TextDecoder();
|
|
412
|
+
const encoder = new TextEncoder();
|
|
413
|
+
const transform = new TransformStream({
|
|
414
|
+
async transform(chunk, controller) {
|
|
415
|
+
if (headInjected) {
|
|
416
|
+
controller.enqueue(chunk);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
headBuffered += decoder.decode(chunk, { stream: true });
|
|
420
|
+
if (headBuffered.includes("</head>") || headBuffered.includes("<body") || headBuffered.length > 8192) {
|
|
421
|
+
headInjected = true;
|
|
422
|
+
let html = await injectFOUCStyles(server, headBuffered);
|
|
423
|
+
html = await server.transformIndexHtml(req.url, html);
|
|
424
|
+
controller.enqueue(encoder.encode(html));
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
async flush(controller) {
|
|
428
|
+
if (!headInjected) {
|
|
429
|
+
headBuffered += decoder.decode();
|
|
430
|
+
let html = await injectFOUCStyles(server, headBuffered);
|
|
431
|
+
html = await server.transformIndexHtml(req.url, html);
|
|
432
|
+
controller.enqueue(encoder.encode(html));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
try {
|
|
437
|
+
await pipeline(Readable.fromWeb(response.body.pipeThrough(transform)), res);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (err.code !== "ERR_STREAM_PREMATURE_CLOSE") throw err;
|
|
440
|
+
}
|
|
396
441
|
return;
|
|
397
442
|
}
|
|
398
|
-
for (const [key, value] of response.headers) res.setHeader(key, value);
|
|
399
443
|
if (req.method === "HEAD" || !response.body) {
|
|
400
|
-
await response.body
|
|
444
|
+
await safeCancelBody(response.body);
|
|
401
445
|
if (!res.destroyed && !res.closed) res.end();
|
|
402
446
|
return;
|
|
403
447
|
}
|
|
404
448
|
if (res.destroyed || res.closed) {
|
|
405
|
-
await response.body
|
|
449
|
+
await safeCancelBody(response.body);
|
|
406
450
|
return;
|
|
407
451
|
}
|
|
408
452
|
try {
|
|
409
453
|
await pipeline(Readable.fromWeb(response.body), res);
|
|
410
|
-
} catch {
|
|
454
|
+
} catch (err) {
|
|
455
|
+
if (err.code !== "ERR_STREAM_PREMATURE_CLOSE") throw err;
|
|
456
|
+
}
|
|
411
457
|
} catch (error) {
|
|
412
458
|
next(error);
|
|
413
459
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vike-lite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@types/node": "^26.1.1",
|
|
85
|
-
"tsdown": "^0.22.
|
|
85
|
+
"tsdown": "^0.22.13",
|
|
86
86
|
"vite": "^8.1.5",
|
|
87
87
|
"vitest": "^4.1.10"
|
|
88
88
|
},
|