vike-lite 1.15.20 → 1.16.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.
@@ -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 `<!DOCTYPE html>
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">${appHtml}</div>
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 };
@@ -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 wrapServerHydration ? `import { onRenderHtml as _onRenderHtml } from '${packageName}/__internal/server/onRenderHtml';export const onRenderHtml = (ctx) => _onRenderHtml({ ...ctx, hydration: ${hydration} });` : `export{onRenderHtml}from'${packageName}/__internal/server/onRenderHtml';`;
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,25 +377,63 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
377
377
  method: req.method,
378
378
  headers
379
379
  };
380
- if (req.url.startsWith(apiPrefix)) {
381
- server.config.logger.info(`⚡ API: ${req.method} ${req.url}`, { timestamp: true });
382
- if (req.method !== "GET" && req.method !== "HEAD") {
383
- requestInit.body = Readable.toWeb(req);
384
- requestInit.duplex = "half";
385
- }
386
- } else if (req.url.endsWith(".pageContext.json")) server.config.logger.info(`🔄 SPA Navigation: ${req.url}`, { timestamp: true });
387
- const response = await app.fetch(new Request(`http://${req.headers.host}${req.url}`, requestInit));
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);
389
392
  if (response.headers.get("content-type")?.includes("text/html")) {
393
+ res.removeHeader("content-length");
390
394
  server.config.logger.info(`📄 Page: ${req.url}`, { timestamp: true });
391
- let html = await response.text();
392
- html = await injectFOUCStyles(server, html);
393
- html = await server.transformIndexHtml(req.url, html);
394
- res.setHeader("Content-Type", "text/html");
395
- res.end(html);
395
+ if (req.method === "HEAD" || !response.body) {
396
+ await response.body?.cancel();
397
+ return res.end();
398
+ }
399
+ if (res.destroyed || res.closed) {
400
+ await response.body.cancel();
401
+ return;
402
+ }
403
+ let headBuffered = "";
404
+ let headInjected = false;
405
+ const decoder = new TextDecoder();
406
+ const encoder = new TextEncoder();
407
+ const transform = new TransformStream({
408
+ async transform(chunk, controller) {
409
+ if (headInjected) {
410
+ controller.enqueue(chunk);
411
+ return;
412
+ }
413
+ headBuffered += decoder.decode(chunk, { stream: true });
414
+ if (headBuffered.includes("</head>") || headBuffered.includes("<body") || headBuffered.length > 8192) {
415
+ headInjected = true;
416
+ let html = await injectFOUCStyles(server, headBuffered);
417
+ html = await server.transformIndexHtml(req.url, html);
418
+ controller.enqueue(encoder.encode(html));
419
+ }
420
+ },
421
+ async flush(controller) {
422
+ if (!headInjected) {
423
+ headBuffered += decoder.decode();
424
+ let html = await injectFOUCStyles(server, headBuffered);
425
+ html = await server.transformIndexHtml(req.url, html);
426
+ controller.enqueue(encoder.encode(html));
427
+ }
428
+ }
429
+ });
430
+ try {
431
+ await pipeline(Readable.fromWeb(response.body.pipeThrough(transform)), res);
432
+ } catch (err) {
433
+ if (err.code !== "ERR_STREAM_PREMATURE_CLOSE") throw err;
434
+ }
396
435
  return;
397
436
  }
398
- for (const [key, value] of response.headers) res.setHeader(key, value);
399
437
  if (req.method === "HEAD" || !response.body) {
400
438
  await response.body?.cancel();
401
439
  if (!res.destroyed && !res.closed) res.end();
@@ -407,7 +445,9 @@ function vikeLite({ pagesDir = "pages", apiPrefix = "/api", prerender = false, s
407
445
  }
408
446
  try {
409
447
  await pipeline(Readable.fromWeb(response.body), res);
410
- } catch {}
448
+ } catch (err) {
449
+ if (err.code !== "ERR_STREAM_PREMATURE_CLOSE") throw err;
450
+ }
411
451
  } catch (error) {
412
452
  next(error);
413
453
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vike-lite",
3
- "version": "1.15.20",
3
+ "version": "1.16.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",