@nuxt/nitro-server-nightly 5.0.0-29662437.89c933ea → 5.0.0-29664396.5668d45c

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
@@ -2,7 +2,7 @@ import { EventHandler, H3Event } from "nitro/h3";
2
2
  import { Nuxt } from "@nuxt/schema";
3
3
  import { Nitro, NitroConfig, NitroDevEventHandler, NitroEventHandler, NitroOptions, NitroRouteConfig, NitroRuntimeConfig, NitroRuntimeConfigApp } from "nitro/types";
4
4
  import { LogObject } from "consola";
5
- import { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from "nuxt/app";
5
+ import { NuxtIslandContext, NuxtIslandResponse, NuxtRenderChunkContext, NuxtRenderCloseContext, NuxtRenderHTMLContext, NuxtRenderRouteContext } from "nuxt/app";
6
6
  import { HookResult, RuntimeConfig, TSReference } from "nuxt/schema";
7
7
 
8
8
  //#region src/augments.d.ts
@@ -14,6 +14,7 @@ declare module "nitro/types" {
14
14
  }
15
15
  interface NitroRouteRules {
16
16
  ssr?: boolean;
17
+ streaming?: boolean;
17
18
  noScripts?: boolean;
18
19
  /** @deprecated Use `noScripts` instead */
19
20
  experimentalNoScripts?: boolean;
@@ -24,6 +25,7 @@ declare module "nitro/types" {
24
25
  interface NitroRuntimeConfig extends RuntimeConfig {}
25
26
  interface NitroRouteConfig {
26
27
  ssr?: boolean;
28
+ streaming?: boolean;
27
29
  noScripts?: boolean;
28
30
  /** @deprecated Use `noScripts` instead */
29
31
  experimentalNoScripts?: boolean;
@@ -35,6 +37,16 @@ declare module "nitro/types" {
35
37
  }) => void | Promise<void>;
36
38
  "render:html": (htmlContext: NuxtRenderHTMLContext, context: {
37
39
  event: H3Event;
40
+ streaming?: boolean;
41
+ }) => void | Promise<void>;
42
+ "render:html:chunk": (chunkContext: NuxtRenderChunkContext, context: {
43
+ event: H3Event;
44
+ }) => void | Promise<void>;
45
+ "render:html:close": (closeContext: NuxtRenderCloseContext, context: {
46
+ event: H3Event;
47
+ }) => void | Promise<void>;
48
+ "render:route": (renderRouteContext: NuxtRenderRouteContext, context: {
49
+ event: H3Event;
38
50
  }) => void | Promise<void>;
39
51
  "render:island": (islandResponse: NuxtIslandResponse, context: {
40
52
  event: H3Event;
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ import { resolveModulePath } from "exsolve";
19
19
  import { runtimeDependencies } from "nitro/meta";
20
20
  import { resolveModuleExportNames } from "mlly";
21
21
  //#region package.json
22
- var version = "5.0.0-29662437.89c933ea";
22
+ var version = "5.0.0-29664396.5668d45c";
23
23
  //#endregion
24
24
  //#region src/utils.ts
25
25
  function toArray(value) {
@@ -124,7 +124,7 @@ ${[...references.map((ref) => renderReference(ref, typesDir)), ...declarations].
124
124
  import type { RuntimeConfig } from 'nuxt/schema'
125
125
  import type { H3Event } from 'nitro/h3'
126
126
  import type { LogObject } from 'consola'
127
- import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from 'nuxt/app'
127
+ import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderChunkContext, NuxtRenderCloseContext, NuxtRenderHTMLContext, NuxtRenderRouteContext } from 'nuxt/app'
128
128
 
129
129
  declare module 'nitro/types' {
130
130
  interface NitroRuntimeConfigApp {
@@ -135,12 +135,14 @@ declare module 'nitro/types' {
135
135
  interface NitroRuntimeConfig extends RuntimeConfig {}
136
136
  interface NitroRouteConfig {
137
137
  ssr?: boolean
138
+ streaming?: boolean
138
139
  noScripts?: boolean
139
140
  /** @deprecated Use \`noScripts\` instead */
140
141
  experimentalNoScripts?: boolean
141
142
  }
142
143
  interface NitroRouteRules {
143
144
  ssr?: boolean
145
+ streaming?: boolean
144
146
  noScripts?: boolean
145
147
  /** @deprecated Use \`noScripts\` instead */
146
148
  experimentalNoScripts?: boolean
@@ -149,7 +151,10 @@ declare module 'nitro/types' {
149
151
  }
150
152
  interface NitroRuntimeHooks {
151
153
  'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
152
- 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
154
+ 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event, streaming?: boolean }) => void | Promise<void>
155
+ 'render:html:chunk': (chunkContext: NuxtRenderChunkContext, context: { event: H3Event }) => void | Promise<void>
156
+ 'render:html:close': (closeContext: NuxtRenderCloseContext, context: { event: H3Event }) => void | Promise<void>
157
+ 'render:route': (renderRouteContext: NuxtRenderRouteContext, context: { event: H3Event }) => void | Promise<void>
153
158
  'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
154
159
  }
155
160
  }
@@ -337,6 +342,7 @@ async function bundle(nuxt) {
337
342
  "#internal/nuxt/app-config": () => nuxt.vfs["#build/app.config.mjs"]?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, "") || "",
338
343
  "#spa-template": async () => `export const template = ${JSON.stringify(await spaLoadingTemplate(nuxt))}`,
339
344
  "#internal/entry-chunk.mjs": () => `export const entryFileName = undefined`,
345
+ "#internal/streaming-iife-chunk.mjs": () => `export const iifeChunkFileName = undefined`,
340
346
  "#internal/nuxt/entry-ids.mjs": () => `export default []`,
341
347
  "#internal/nuxt/nitro-config.mjs": () => {
342
348
  const hasCachedRoutes = nitro.routing.routeRules.routes.some((r) => r.data.isr || r.data.cache);
@@ -350,12 +356,17 @@ async function bundle(nuxt) {
350
356
  `export const NUXT_SHARED_DATA = ${!!nuxt.options.experimental.sharedPrerenderData}`,
351
357
  `export const NUXT_PAYLOAD_EXTRACTION = ${nuxt.options.experimental.payloadExtraction !== false}`,
352
358
  `export const NUXT_PAYLOAD_INLINE = ${nuxt.options.experimental.payloadExtraction !== true}`,
353
- `export const NUXT_RUNTIME_PAYLOAD_EXTRACTION = ${hasCachedRoutes}`
359
+ `export const NUXT_RUNTIME_PAYLOAD_EXTRACTION = ${hasCachedRoutes}`,
360
+ `export const NUXT_SSR_STREAMING = ${!!(typeof nuxt.options.experimental.ssrStreaming === "object" && nuxt.options.experimental.ssrStreaming.enabled)}`,
361
+ `export const NUXT_SSR_STREAMING_BOT_RE = ${typeof nuxt.options.experimental.ssrStreaming === "object" && nuxt.options.experimental.ssrStreaming.botRegex instanceof RegExp ? String(nuxt.options.experimental.ssrStreaming.botRegex) : "/^$/"}`
354
362
  ].join("\n");
355
363
  }
356
364
  },
357
365
  routeRules: {
358
- "/**": { ssr: true },
366
+ "/**": typeof nuxt.options.experimental.ssrStreaming === "object" && nuxt.options.experimental.ssrStreaming.enabled ? {
367
+ ssr: true,
368
+ streaming: true
369
+ } : { ssr: true },
359
370
  "/__nuxt_error": { cache: false }
360
371
  },
361
372
  rolldownConfig: { tsconfig: false },
@@ -1,9 +1,9 @@
1
1
  import { payloadCache, prerenderRenderingURLs } from "../utils/cache.mjs";
2
2
  import { createSSRContext, setSSRError } from "../utils/renderer/app.mjs";
3
3
  import { buildAssetsURL, publicAssetsURL } from "../utils/paths.mjs";
4
- import { getRenderer } from "../utils/renderer/build-files.mjs";
4
+ import { APP_ROOT_CLOSE_TAG, APP_ROOT_OPEN_TAG, getRenderer, getServerApp } from "../utils/renderer/build-files.mjs";
5
5
  import { renderInlineStyles } from "../utils/renderer/inline-styles.mjs";
6
- import { replaceIslandTeleports } from "../utils/renderer/islands.mjs";
6
+ import { renderStreamedIslandTeleports, replaceIslandTeleports } from "../utils/renderer/islands.mjs";
7
7
  import { renderPayloadJsonScript, renderPayloadResponse, splitPayload } from "../utils/renderer/payload.mjs";
8
8
  import { getQuery, joinURL } from "ufo";
9
9
  import { getRouteRules, useNitroHooks } from "nitro/app";
@@ -11,13 +11,17 @@ import destr$1 from "destr";
11
11
  import { HTTPError, defineEventHandler, getQuery as getQuery$1, writeEarlyHints } from "nitro/h3";
12
12
  import { getPrefetchLinks, getPreloadLinks, getRequestDependencies, renderResourceHeaders } from "vue-bundle-renderer/runtime";
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
- import { NUXT_ASYNC_CONTEXT, NUXT_EARLY_HINTS, NUXT_INLINE_STYLES, NUXT_NO_SCRIPTS, NUXT_PAYLOAD_EXTRACTION, NUXT_PAYLOAD_INLINE, NUXT_RUNTIME_PAYLOAD_EXTRACTION, PARSE_ERROR_DATA } from "#internal/nuxt/nitro-config.mjs";
14
+ import { NUXT_ASYNC_CONTEXT, NUXT_EARLY_HINTS, NUXT_INLINE_STYLES, NUXT_NO_SCRIPTS, NUXT_PAYLOAD_EXTRACTION, NUXT_PAYLOAD_INLINE, NUXT_RUNTIME_PAYLOAD_EXTRACTION, NUXT_SSR_STREAMING, NUXT_SSR_STREAMING_BOT_RE, PARSE_ERROR_DATA } from "#internal/nuxt/nitro-config.mjs";
15
15
  import { propsToString, renderSSRHead } from "@unhead/vue/server";
16
- import { appHead, appTeleportAttrs, appTeleportTag, componentIslands } from "#internal/nuxt.config.mjs";
16
+ import { renderToWebStream } from "vue/server-renderer";
17
+ import { appHead, appTeleportAttrs, appTeleportTag, componentIslands, componentIslandsActive } from "#internal/nuxt.config.mjs";
18
+ import { createBootstrapScript, renderSSRHeadSuspenseChunk, renderShell } from "@unhead/vue/stream/server";
19
+ import { streamingIifeCode } from "@unhead/vue/stream/iife";
17
20
  import { relative } from "pathe";
18
21
  import { renderSSRHeadOptions } from "#internal/unhead.config.mjs";
19
22
  import entryIds from "#internal/nuxt/entry-ids.mjs";
20
23
  import { entryFileName } from "#internal/entry-chunk.mjs";
24
+ import { iifeChunkFileName } from "#internal/streaming-iife-chunk.mjs";
21
25
  //#region src/runtime/handlers/renderer.ts
22
26
  globalThis.__buildAssetsURL = buildAssetsURL;
23
27
  globalThis.__publicAssetsURL = publicAssetsURL;
@@ -28,6 +32,7 @@ const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : "";
28
32
  const PAYLOAD_URL_RE = /^[^?]*\/_payload.json(?:\?.*)?$/;
29
33
  const PAYLOAD_FILENAME = "_payload.json";
30
34
  let entryPath;
35
+ const SSR_BOT_RE = NUXT_SSR_STREAMING_BOT_RE;
31
36
  const handler = defineEventHandler((event) => {
32
37
  const ssrError = event.url.pathname.startsWith("/__nuxt_error") ? getQuery$1(event) : null;
33
38
  if (ssrError && !event.context.nuxt?.["~internal"]) throw new HTTPError({
@@ -72,17 +77,37 @@ async function renderRoute(event, ssrError) {
72
77
  if (link) writeEarlyHints(event, { link });
73
78
  }
74
79
  if (NUXT_INLINE_STYLES) for (const id of entryIds) ssrContext.modules.add(id);
80
+ const canStream = NUXT_SSR_STREAMING && !ssrContext.noSSR && !ssrError && !isRenderingPayload && !import.meta.prerender && !(NUXT_NO_SCRIPTS && componentIslandsActive) && !routeOptions.noScripts && !!routeOptions.streaming && !routeOptions.cache && !routeOptions.isr && !routeOptions.redirect;
81
+ const renderRouteContext = {
82
+ canStream,
83
+ prefersStream: !!(NUXT_SSR_STREAMING && !SSR_BOT_RE.test(event.req.headers.get("user-agent") || ""))
84
+ };
85
+ const nitroHooks = useNitroHooks();
86
+ const renderRouteResult = nitroHooks.callHook("render:route", renderRouteContext, { event });
87
+ if (renderRouteResult instanceof Promise) await renderRouteResult;
88
+ if (NUXT_SSR_STREAMING && canStream && renderRouteContext.prefersStream) return renderStreamedResponse({
89
+ event,
90
+ ssrContext,
91
+ renderer,
92
+ routeOptions,
93
+ ssrError,
94
+ _PAYLOAD_EXTRACTION,
95
+ _PAYLOAD_INLINE,
96
+ payloadURL
97
+ });
75
98
  const _rendered = await renderer.renderToString(ssrContext).catch(async (error) => {
76
99
  if (ssrContext["~renderResponse"] && error.message === "skipping render") return {};
77
100
  const _err = !ssrError && ssrContext.payload?.error || error;
78
- await ssrContext.nuxt?.hooks.callHook("app:error", _err);
101
+ const r = ssrContext.nuxt?.hooks.callHook("app:error", _err);
102
+ if (r instanceof Promise) await r;
79
103
  throw _err;
80
104
  });
81
105
  const inlinedStyles = NUXT_INLINE_STYLES && !ssrContext["~renderResponse"] && !isRenderingPayload ? await renderInlineStyles(ssrContext.modules ?? []) : [];
82
- await ssrContext.nuxt?.hooks.callHook("app:rendered", {
106
+ const appRenderedResult = ssrContext.nuxt?.hooks.callHook("app:rendered", {
83
107
  ssrContext,
84
108
  renderResult: _rendered
85
109
  });
110
+ if (appRenderedResult instanceof Promise) await appRenderedResult;
86
111
  if (ssrContext["~renderResponse"]) return returnResponse(event, ssrContext["~renderResponse"]);
87
112
  if (ssrContext.payload?.error && !ssrError) throw ssrContext.payload.error;
88
113
  if (isRenderingPayload) {
@@ -107,6 +132,8 @@ async function renderRoute(event, ssrError) {
107
132
  }
108
133
  }
109
134
  ssrContext.head.push({ script: [{
135
+ tagPosition: "head",
136
+ tagPriority: "critical",
110
137
  type: "importmap",
111
138
  innerHTML: { imports: { "#entry": path } }
112
139
  }] });
@@ -160,11 +187,272 @@ async function renderRoute(event, ssrError) {
160
187
  body: [componentIslands ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html, APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportAttrs.id}`]]) : "") + APP_TELEPORT_CLOSE_TAG],
161
188
  bodyAppend: [bodyTags]
162
189
  };
163
- await useNitroHooks().callHook("render:html", htmlContext, { event });
190
+ const renderHtmlResult = nitroHooks.callHook("render:html", htmlContext, { event });
191
+ if (renderHtmlResult instanceof Promise) await renderHtmlResult;
164
192
  event.res.headers.set("content-type", "text/html;charset=utf-8");
165
193
  event.res.headers.set("x-powered-by", "Nuxt");
166
194
  return renderHTMLDocument(htmlContext);
167
195
  }
196
+ async function renderStreamedResponse(ctx) {
197
+ const { event, ssrContext, renderer, routeOptions, ssrError, _PAYLOAD_EXTRACTION, _PAYLOAD_INLINE, payloadURL } = ctx;
198
+ const NO_SCRIPTS = NUXT_NO_SCRIPTS || !!routeOptions?.noScripts;
199
+ const { link: linkHeader } = renderResourceHeaders({}, renderer.rendererContext);
200
+ if (linkHeader) event.res.headers.append("link", linkHeader);
201
+ const entryInlineStyles = NUXT_INLINE_STYLES ? await renderInlineStyles(new Set(entryIds)) : [];
202
+ if (entryInlineStyles.length) ssrContext.head.push({ style: entryInlineStyles });
203
+ const { styles: entryStyles, scripts: entryScripts } = getRequestDependencies({}, renderer.rendererContext);
204
+ const shellLinks = [];
205
+ for (const resource of Object.values(entryStyles)) {
206
+ if (import.meta.dev && "inline" in getQuery(resource.file)) continue;
207
+ shellLinks.push({
208
+ rel: "stylesheet",
209
+ href: renderer.rendererContext.buildAssetsURL(resource.file),
210
+ crossorigin: ""
211
+ });
212
+ }
213
+ if (shellLinks.length) ssrContext.head.push({ link: shellLinks });
214
+ if (entryFileName && !NO_SCRIPTS) {
215
+ let path = entryPath;
216
+ if (!path) {
217
+ path = buildAssetsURL(entryFileName);
218
+ if (ssrContext.runtimeConfig.app.cdnURL || /^(?:\/|\.+\/)/.test(path)) entryPath = path;
219
+ else {
220
+ path = relative(event.url.pathname.replace(/\/[^/]+$/, "/"), joinURL("/", path));
221
+ if (!/^(?:\/|\.+\/)/.test(path)) path = `./${path}`;
222
+ }
223
+ }
224
+ ssrContext.head.push({ script: [{
225
+ tagPosition: "head",
226
+ tagPriority: "critical",
227
+ type: "importmap",
228
+ innerHTML: { imports: { "#entry": path } }
229
+ }] });
230
+ }
231
+ if (_PAYLOAD_EXTRACTION && !_PAYLOAD_INLINE && !NO_SCRIPTS) ssrContext.head.push({ link: [{
232
+ rel: "preload",
233
+ as: "fetch",
234
+ crossorigin: "anonymous",
235
+ href: payloadURL
236
+ }] });
237
+ if (!NO_SCRIPTS) {
238
+ ssrContext.head.push({ link: getPreloadLinks({}, renderer.rendererContext) });
239
+ ssrContext.head.push({ link: getPrefetchLinks({}, renderer.rendererContext) });
240
+ }
241
+ if (!NO_SCRIPTS) ssrContext.head.push({ script: Object.values(entryScripts).map((resource) => ({
242
+ type: resource.module ? "module" : null,
243
+ src: renderer.rendererContext.buildAssetsURL(resource.file),
244
+ defer: resource.module ? null : true,
245
+ tagPosition: "head",
246
+ crossorigin: ""
247
+ })) });
248
+ if (!NO_SCRIPTS && !import.meta.dev && iifeChunkFileName) ssrContext.head.push({ link: [{
249
+ rel: "preload",
250
+ as: "script",
251
+ href: buildAssetsURL(iifeChunkFileName)
252
+ }] });
253
+ const createSSRApp = await getServerApp();
254
+ let vueApp;
255
+ try {
256
+ vueApp = await createSSRApp(ssrContext);
257
+ } catch (error) {
258
+ if (ssrContext["~renderResponse"] && error?.message === "skipping render") {
259
+ event.res.headers.delete("link");
260
+ return returnResponse(event, ssrContext["~renderResponse"]);
261
+ }
262
+ const r = ssrContext.nuxt?.hooks.callHook("app:error", error);
263
+ if (r instanceof Promise) await r;
264
+ throw error;
265
+ }
266
+ if (ssrContext["~renderResponse"]) {
267
+ event.res.headers.delete("link");
268
+ return returnResponse(event, ssrContext["~renderResponse"]);
269
+ }
270
+ const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = renderShell(ssrContext.head);
271
+ const cspNonce = headTags.match(/<script[^>]+\bnonce="([^"]*)"/)?.[1];
272
+ const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
273
+ const bootstrapScript = NO_SCRIPTS ? "" : createBootstrapScript(void 0, cspNonce);
274
+ let iifeScript = "";
275
+ if (!NO_SCRIPTS) if (!import.meta.dev && iifeChunkFileName) iifeScript = `<script async${nonceAttr} src="${buildAssetsURL(iifeChunkFileName)}"><\/script>`;
276
+ else iifeScript = `<script${nonceAttr}>${streamingIifeCode}<\/script>`;
277
+ const shellContext = {
278
+ htmlAttrs: htmlAttrs ? [htmlAttrs] : [],
279
+ head: normalizeChunks([bootstrapScript, headTags]),
280
+ bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
281
+ bodyPrepend: normalizeChunks([iifeScript, bodyTagsOpen]),
282
+ body: [],
283
+ bodyAppend: []
284
+ };
285
+ const nitroHooks = useNitroHooks();
286
+ if (import.meta.dev) {
287
+ const initialBodyLen = shellContext.body.length;
288
+ const initialAppendLen = shellContext.bodyAppend.length;
289
+ const r = nitroHooks.callHook("render:html", shellContext, {
290
+ event,
291
+ streaming: true
292
+ });
293
+ if (r instanceof Promise) await r;
294
+ if (shellContext.body.length !== initialBodyLen || shellContext.bodyAppend.length !== initialAppendLen) console.warn(`[nuxt] \`render:html\` mutated \`body\`/\`bodyAppend\` while streaming (${event.url.pathname}). These fields are silently dropped because the body is about to stream - use the \`render:html:close\` hook instead.`);
295
+ } else {
296
+ const r = nitroHooks.callHook("render:html", shellContext, {
297
+ event,
298
+ streaming: true
299
+ });
300
+ if (r instanceof Promise) await r;
301
+ }
302
+ const shellHtml = `<!DOCTYPE html><html${joinAttrs(shellContext.htmlAttrs)}><head>${joinTags(shellContext.head)}</head><body${joinAttrs(shellContext.bodyAttrs)}>` + joinTags(shellContext.bodyPrepend);
303
+ const reader = renderToWebStream(vueApp, ssrContext).getReader();
304
+ let firstChunk;
305
+ try {
306
+ const { done, value } = await reader.read();
307
+ if (!done) firstChunk = value;
308
+ } catch (error) {
309
+ reader.releaseLock();
310
+ event.res.headers.delete("link");
311
+ if (ssrContext["~renderResponse"]) return returnResponse(event, ssrContext["~renderResponse"]);
312
+ const _err = !ssrError && ssrContext.payload?.error || error;
313
+ const r = ssrContext.nuxt?.hooks.callHook("app:error", _err);
314
+ if (r instanceof Promise) await r;
315
+ throw _err;
316
+ }
317
+ if (ssrContext["~renderResponse"]) {
318
+ reader.cancel().catch(() => {});
319
+ event.res.headers.delete("link");
320
+ return returnResponse(event, ssrContext["~renderResponse"]);
321
+ }
322
+ if (ssrContext.payload?.error && !ssrError) {
323
+ reader.cancel().catch(() => {});
324
+ event.res.headers.delete("link");
325
+ throw ssrContext.payload.error;
326
+ }
327
+ const committedSnapshot = import.meta.dev ? {
328
+ status: event.res.status,
329
+ statusText: event.res.statusText,
330
+ headers: Array.from(event.res.headers.entries()).sort().map(([k, v]) => `${k}: ${v}`).join("\n")
331
+ } : null;
332
+ const encoder = new TextEncoder();
333
+ let chunkIndex = 0;
334
+ const enqueueChunk = (controller, chunk) => {
335
+ const chunkContext = {
336
+ chunk,
337
+ index: chunkIndex++
338
+ };
339
+ const result = nitroHooks.callHook("render:html:chunk", chunkContext, { event });
340
+ if (result instanceof Promise) return result.then(() => {
341
+ controller.enqueue(chunkContext.chunk);
342
+ });
343
+ controller.enqueue(chunkContext.chunk);
344
+ };
345
+ const emittedStyles = new Set(Object.values(entryStyles).map((r) => r.file));
346
+ const inlinedCss = new Set(entryInlineStyles.map((s) => String(s.innerHTML)));
347
+ const renderRouteStyles = async () => {
348
+ let tags = "";
349
+ if (NUXT_INLINE_STYLES) {
350
+ for (const style of await renderInlineStyles(ssrContext.modules ?? [])) {
351
+ const css = String(style.innerHTML);
352
+ if (!css || inlinedCss.has(css)) continue;
353
+ inlinedCss.add(css);
354
+ tags += `<style${nonceAttr}>${css}</style>`;
355
+ }
356
+ return tags;
357
+ }
358
+ for (const resource of Object.values(getRequestDependencies(ssrContext, renderer.rendererContext).styles)) {
359
+ if (emittedStyles.has(resource.file)) continue;
360
+ if (import.meta.dev && "inline" in getQuery(resource.file)) continue;
361
+ emittedStyles.add(resource.file);
362
+ tags += `<link rel="stylesheet" crossorigin href="${renderer.rendererContext.buildAssetsURL(resource.file)}">`;
363
+ }
364
+ return tags;
365
+ };
366
+ const outputStream = new ReadableStream({
367
+ async start(controller) {
368
+ try {
369
+ await enqueueChunk(controller, encoder.encode(shellHtml));
370
+ await enqueueChunk(controller, encoder.encode(await renderRouteStyles() + APP_ROOT_OPEN_TAG));
371
+ if (firstChunk) {
372
+ await enqueueChunk(controller, firstChunk);
373
+ const headChunk = renderSSRHeadSuspenseChunk(ssrContext.head);
374
+ if (headChunk && !NO_SCRIPTS) await enqueueChunk(controller, encoder.encode(`<script${nonceAttr}>${headChunk};document.currentScript.remove()<\/script>`));
375
+ }
376
+ try {
377
+ while (true) {
378
+ const { done, value } = await reader.read();
379
+ if (done) break;
380
+ await enqueueChunk(controller, value);
381
+ const headChunk = renderSSRHeadSuspenseChunk(ssrContext.head);
382
+ if (headChunk && !NO_SCRIPTS) await enqueueChunk(controller, encoder.encode(`<script${nonceAttr}>${headChunk};document.currentScript.remove()<\/script>`));
383
+ }
384
+ } finally {
385
+ reader.releaseLock();
386
+ }
387
+ if (!NO_SCRIPTS) {
388
+ const finalHeadChunk = renderSSRHeadSuspenseChunk(ssrContext.head);
389
+ if (finalHeadChunk) await enqueueChunk(controller, encoder.encode(`<script${nonceAttr}>${finalHeadChunk};document.currentScript.remove()<\/script>`));
390
+ }
391
+ const appRenderedResult = ssrContext.nuxt?.hooks.callHook("app:rendered", {
392
+ ssrContext,
393
+ renderResult: {}
394
+ });
395
+ if (appRenderedResult instanceof Promise) await appRenderedResult;
396
+ if (ssrContext.payload?.error && !ssrError) {
397
+ const r = ssrContext.nuxt?.hooks.callHook("app:error", ssrContext.payload.error);
398
+ if (r instanceof Promise) await r;
399
+ }
400
+ if (!NO_SCRIPTS) ssrContext.head.push({ script: _PAYLOAD_INLINE ? renderPayloadJsonScript({
401
+ ssrContext,
402
+ data: ssrContext.payload
403
+ }) : renderPayloadJsonScript({
404
+ ssrContext,
405
+ data: splitPayload(ssrContext).initial,
406
+ src: payloadURL
407
+ }) }, {
408
+ tagPosition: "bodyClose",
409
+ tagPriority: "high"
410
+ });
411
+ const closeContext = { bodyAppend: normalizeChunks([bodyTags, applyRenderOptions(ssrContext.head.render(), renderSSRHeadOptions).bodyTags]) };
412
+ const closeResult = nitroHooks.callHook("render:html:close", closeContext, { event });
413
+ if (closeResult instanceof Promise) await closeResult;
414
+ const teleportHtml = APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportAttrs.id}`]]) : "") + APP_TELEPORT_CLOSE_TAG;
415
+ const islandTeleports = NO_SCRIPTS ? "" : renderStreamedIslandTeleports(ssrContext, nonceAttr);
416
+ const closingHtml = APP_ROOT_CLOSE_TAG + await renderRouteStyles() + teleportHtml + (ssrContext.teleports?.body || "") + islandTeleports + joinTags(closeContext.bodyAppend) + "</body></html>";
417
+ await enqueueChunk(controller, encoder.encode(closingHtml));
418
+ controller.close();
419
+ if (committedSnapshot) {
420
+ const currentHeaders = Array.from(event.res.headers.entries()).sort().map(([k, v]) => `${k}: ${v}`).join("\n");
421
+ const lateMutations = [];
422
+ if (event.res.status !== committedSnapshot.status) lateMutations.push(`response status changed from ${committedSnapshot.status || 200} to ${event.res.status} (e.g. \`setResponseStatus\`)`);
423
+ if (event.res.statusText !== committedSnapshot.statusText) lateMutations.push(`response statusText changed (e.g. \`setResponseStatus\`)`);
424
+ if (currentHeaders !== committedSnapshot.headers) lateMutations.push(`response headers changed during render (e.g. \`useCookie\`, \`useResponseHeader\`, \`setHeader\`)`);
425
+ if (lateMutations.length) console.warn(`[nuxt] SSR streaming committed the response before render completed. The following mutations did not reach the client and were dropped:\n - ${lateMutations.join("\n - ")}\n Path: ${event.url.pathname}\n Move the mutation into a plugin (which runs before the shell is flushed), or opt this route out of streaming with \`routeRules: { '${event.url.pathname}': { streaming: false } }\` or the \`render:route\` hook.`);
426
+ }
427
+ } catch (error) {
428
+ await Promise.resolve(ssrContext.nuxt?.hooks.callHook("app:error", error)).catch(() => {});
429
+ ssrContext.payload ||= {};
430
+ ssrContext.payload.error ||= error;
431
+ try {
432
+ if (!NO_SCRIPTS) {
433
+ ssrContext.head.push({ script: renderPayloadJsonScript({
434
+ ssrContext,
435
+ data: ssrContext.payload
436
+ }) }, {
437
+ tagPosition: "bodyClose",
438
+ tagPriority: "high"
439
+ });
440
+ const tail = applyRenderOptions(ssrContext.head.render(), renderSSRHeadOptions);
441
+ controller.enqueue(encoder.encode(tail.bodyTags));
442
+ }
443
+ } catch {}
444
+ controller.enqueue(encoder.encode(APP_ROOT_CLOSE_TAG + "</body></html>"));
445
+ controller.close();
446
+ }
447
+ },
448
+ cancel(reason) {
449
+ reader.cancel(reason).catch(() => {});
450
+ }
451
+ });
452
+ event.res.headers.set("content-type", "text/html;charset=utf-8");
453
+ event.res.headers.set("x-powered-by", "Nuxt");
454
+ return outputStream;
455
+ }
168
456
  function normalizeChunks(chunks) {
169
457
  const result = [];
170
458
  for (const _chunk of chunks) {
@@ -183,6 +471,16 @@ function joinAttrs(chunks) {
183
471
  function renderHTMLDocument(html) {
184
472
  return `<!DOCTYPE html><html${joinAttrs(html.htmlAttrs)}><head>${joinTags(html.head)}</head><body${joinAttrs(html.bodyAttrs)}>${joinTags(html.bodyPrepend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}</body></html>`;
185
473
  }
474
+ function applyRenderOptions(payload, options) {
475
+ if (!options.omitLineBreaks) return payload;
476
+ return {
477
+ headTags: payload.headTags.replaceAll("\n", ""),
478
+ bodyTags: payload.bodyTags.replaceAll("\n", ""),
479
+ bodyTagsOpen: payload.bodyTagsOpen.replaceAll("\n", ""),
480
+ htmlAttrs: payload.htmlAttrs,
481
+ bodyAttrs: payload.bodyAttrs
482
+ };
483
+ }
186
484
  function stripInlineOnlyPayloadFields(payload) {
187
485
  if (!payload.prefetchLinks) return payload;
188
486
  const { prefetchLinks: _, ...rest } = payload;
@@ -73,6 +73,7 @@ function lazyCachedFunction(fn) {
73
73
  function getRenderer(ssrContext) {
74
74
  return NUXT_NO_SSR || ssrContext.noSSR ? getSPARenderer() : getSSRRenderer();
75
75
  }
76
+ const getServerApp = lazyCachedFunction(getServerEntry);
76
77
  const getSSRStyles = lazyCachedFunction(() => import("#build/dist/server/styles.mjs").then((r) => r.default || r));
77
78
  //#endregion
78
- export { getRenderer, getSSRRenderer, getSSRStyles };
79
+ export { APP_ROOT_CLOSE_TAG, APP_ROOT_OPEN_TAG, getRenderer, getSSRRenderer, getSSRStyles, getServerApp };
@@ -45,6 +45,34 @@ function getComponentSlotTeleport(clientUid, teleports) {
45
45
  }
46
46
  return slots;
47
47
  }
48
+ const ISLAND_TELEPORT_RELOCATE_SCRIPT = `(()=>{for(const t of document.querySelectorAll('template[data-island-uid]')){const u=t.getAttribute('data-island-uid'),s=t.getAttribute('data-island-slot'),c=t.getAttribute('data-island-component'),a=document.querySelector('[data-island-uid="'+u+'"]'+(s!==null?'[data-island-slot="'+s+'"]':'[data-island-component="'+c+'"]'));if(a){a.insertBefore(t.content,a.firstChild)}t.remove()}})()`;
49
+ /**
50
+ * Emit each island teleport as an inert `<template>` keyed by its anchor,
51
+ * followed by a relocation script that moves the content into place before
52
+ * hydration. Returns an empty string when there are no island teleports.
53
+ */
54
+ function renderStreamedIslandTeleports(ssrContext, nonceAttr = "") {
55
+ const { teleports, islandContext } = ssrContext;
56
+ if (islandContext || !teleports) return "";
57
+ let templates = "";
58
+ for (const key in teleports) {
59
+ const matchClientComp = key.match(SSR_CLIENT_TELEPORT_MARKER);
60
+ if (matchClientComp) {
61
+ const [, uid, clientId] = matchClientComp;
62
+ if (!uid || !clientId) continue;
63
+ templates += `<template data-island-uid="${uid}" data-island-component="${clientId}">${teleports[key]}</template>`;
64
+ continue;
65
+ }
66
+ const matchSlot = key.match(SSR_SLOT_TELEPORT_MARKER);
67
+ if (matchSlot) {
68
+ const [, uid, slot] = matchSlot;
69
+ if (!uid || !slot) continue;
70
+ templates += `<template data-island-uid="${uid}" data-island-slot="${slot}">${teleports[key]}</template>`;
71
+ }
72
+ }
73
+ if (!templates) return "";
74
+ return templates + `<script${nonceAttr}>${ISLAND_TELEPORT_RELOCATE_SCRIPT}<\/script>`;
75
+ }
48
76
  function replaceIslandTeleports(ssrContext, html) {
49
77
  const { teleports, islandContext } = ssrContext;
50
78
  if (islandContext || !teleports) return html;
@@ -70,4 +98,4 @@ function replaceIslandTeleports(ssrContext, html) {
70
98
  return html;
71
99
  }
72
100
  //#endregion
73
- export { getClientIslandResponse, getComponentSlotTeleport, getServerComponentHTML, getSlotIslandResponse, replaceIslandTeleports };
101
+ export { getClientIslandResponse, getComponentSlotTeleport, getServerComponentHTML, getSlotIslandResponse, renderStreamedIslandTeleports, replaceIslandTeleports };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt/nitro-server-nightly",
3
- "version": "5.0.0-29662437.89c933ea",
3
+ "version": "5.0.0-29664396.5668d45c",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nuxt/nuxt.git",
@@ -27,7 +27,7 @@
27
27
  "dist"
28
28
  ],
29
29
  "dependencies": {
30
- "@nuxt/kit": "npm:@nuxt/kit-nightly@5.0.0-29662437.89c933ea",
30
+ "@nuxt/kit": "npm:@nuxt/kit-nightly@5.0.0-29664396.5668d45c",
31
31
  "@unhead/vue": "^3.1.0",
32
32
  "@vue/shared": "3.5.34",
33
33
  "consola": "^3.4.2",
@@ -58,7 +58,7 @@
58
58
  "@babel/plugin-proposal-decorators": "^7.25.0",
59
59
  "@babel/plugin-syntax-typescript": "^7.25.0",
60
60
  "@rollup/plugin-babel": "^6.0.0 || ^7.0.0",
61
- "nuxt": "npm:nuxt-nightly@5.0.0-29662437.89c933ea"
61
+ "nuxt": "npm:nuxt-nightly@5.0.0-29664396.5668d45c"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@babel/plugin-proposal-decorators": {
@@ -74,10 +74,10 @@
74
74
  "devDependencies": {
75
75
  "@babel/plugin-proposal-decorators": "7.29.0",
76
76
  "@babel/plugin-syntax-typescript": "7.28.6",
77
- "@nuxt/schema": "npm:@nuxt/schema-nightly@5.0.0-29662437.89c933ea",
77
+ "@nuxt/schema": "npm:@nuxt/schema-nightly@5.0.0-29664396.5668d45c",
78
78
  "@rollup/plugin-babel": "7.0.0",
79
79
  "hookable": "6.1.1",
80
- "nuxt": "npm:nuxt-nightly@5.0.0-29662437.89c933ea",
80
+ "nuxt": "npm:nuxt-nightly@5.0.0-29664396.5668d45c",
81
81
  "tsdown": "0.22.0",
82
82
  "vitest": "4.1.7"
83
83
  },