@nuxt/nitro-server-nightly 4.5.0-29673761.f253a70d → 4.5.0-29673775.d239dbcd

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 "h3";
2
2
  import { Nuxt } from "@nuxt/schema";
3
3
  import { Nitro, NitroConfig, NitroDevEventHandler, NitroEventHandler, NitroOptions, NitroRouteConfig, NitroRuntimeConfig, NitroRuntimeConfigApp } from "nitropack/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
  declare module "nitropack" {
8
8
  interface NitroRuntimeConfigApp {
@@ -11,6 +11,7 @@ declare module "nitropack" {
11
11
  }
12
12
  interface NitroRouteRules {
13
13
  ssr?: boolean;
14
+ streaming?: boolean;
14
15
  noScripts?: boolean;
15
16
  /** @deprecated Use `noScripts` instead */
16
17
  experimentalNoScripts?: boolean;
@@ -25,6 +26,7 @@ declare module "nitropack/types" {
25
26
  }
26
27
  interface NitroRouteRules {
27
28
  ssr?: boolean;
29
+ streaming?: boolean;
28
30
  noScripts?: boolean;
29
31
  /** @deprecated Use `noScripts` instead */
30
32
  experimentalNoScripts?: boolean;
@@ -36,6 +38,7 @@ declare module "nitropack" {
36
38
  interface NitroRuntimeConfig extends RuntimeConfig {}
37
39
  interface NitroRouteConfig {
38
40
  ssr?: boolean;
41
+ streaming?: boolean;
39
42
  noScripts?: boolean;
40
43
  /** @deprecated Use `noScripts` instead */
41
44
  experimentalNoScripts?: boolean;
@@ -47,6 +50,16 @@ declare module "nitropack" {
47
50
  }) => void | Promise<void>;
48
51
  "render:html": (htmlContext: NuxtRenderHTMLContext, context: {
49
52
  event: H3Event;
53
+ streaming?: boolean;
54
+ }) => void | Promise<void>;
55
+ "render:html:chunk": (chunkContext: NuxtRenderChunkContext, context: {
56
+ event: H3Event;
57
+ }) => void | Promise<void>;
58
+ "render:html:close": (closeContext: NuxtRenderCloseContext, context: {
59
+ event: H3Event;
60
+ }) => void | Promise<void>;
61
+ "render:route": (renderRouteContext: NuxtRenderRouteContext, context: {
62
+ event: H3Event;
50
63
  }) => void | Promise<void>;
51
64
  "render:island": (islandResponse: NuxtIslandResponse, context: {
52
65
  event: H3Event;
@@ -58,6 +71,7 @@ declare module "nitropack/types" {
58
71
  interface NitroRuntimeConfig extends RuntimeConfig {}
59
72
  interface NitroRouteConfig {
60
73
  ssr?: boolean;
74
+ streaming?: boolean;
61
75
  noScripts?: boolean;
62
76
  /** @deprecated Use `noScripts` instead */
63
77
  experimentalNoScripts?: boolean;
@@ -69,6 +83,16 @@ declare module "nitropack/types" {
69
83
  }) => void | Promise<void>;
70
84
  "render:html": (htmlContext: NuxtRenderHTMLContext, context: {
71
85
  event: H3Event;
86
+ streaming?: boolean;
87
+ }) => void | Promise<void>;
88
+ "render:html:chunk": (chunkContext: NuxtRenderChunkContext, context: {
89
+ event: H3Event;
90
+ }) => void | Promise<void>;
91
+ "render:html:close": (closeContext: NuxtRenderCloseContext, context: {
92
+ event: H3Event;
93
+ }) => void | Promise<void>;
94
+ "render:route": (renderRouteContext: NuxtRenderRouteContext, context: {
95
+ event: H3Event;
72
96
  }) => void | Promise<void>;
73
97
  "render:island": (islandResponse: NuxtIslandResponse, context: {
74
98
  event: H3Event;
package/dist/index.mjs CHANGED
@@ -21,7 +21,7 @@ import { hasTTY, isCI, isWindows } from "std-env";
21
21
  import { ImpoundPlugin } from "impound";
22
22
  import { resolveModulePath } from "exsolve";
23
23
  import { runtimeDependencies } from "nitropack/runtime/meta";
24
- var version = "4.5.0-29673761.f253a70d";
24
+ var version = "4.5.0-29673775.d239dbcd";
25
25
  function toArray(value) {
26
26
  return Array.isArray(value) ? value : [value];
27
27
  }
@@ -106,7 +106,7 @@ ${[...references.map((ref) => renderReference(ref, typesDir)), ...declarations].
106
106
  import type { RuntimeConfig } from 'nuxt/schema'
107
107
  import type { H3Event } from 'h3'
108
108
  import type { LogObject } from 'consola'
109
- import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from 'nuxt/app'
109
+ import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderChunkContext, NuxtRenderCloseContext, NuxtRenderHTMLContext, NuxtRenderRouteContext } from 'nuxt/app'
110
110
 
111
111
  declare module 'nitropack' {
112
112
  interface NitroRuntimeConfigApp {
@@ -116,12 +116,14 @@ declare module 'nitropack' {
116
116
  interface NitroRuntimeConfig extends RuntimeConfig {}
117
117
  interface NitroRouteConfig {
118
118
  ssr?: boolean
119
+ streaming?: boolean
119
120
  noScripts?: boolean
120
121
  /** @deprecated Use \`noScripts\` instead */
121
122
  experimentalNoScripts?: boolean
122
123
  }
123
124
  interface NitroRouteRules {
124
125
  ssr?: boolean
126
+ streaming?: boolean
125
127
  noScripts?: boolean
126
128
  /** @deprecated Use \`noScripts\` instead */
127
129
  experimentalNoScripts?: boolean
@@ -130,7 +132,10 @@ declare module 'nitropack' {
130
132
  }
131
133
  interface NitroRuntimeHooks {
132
134
  'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
133
- 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
135
+ 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event, streaming?: boolean }) => void | Promise<void>
136
+ 'render:html:chunk': (chunkContext: NuxtRenderChunkContext, context: { event: H3Event }) => void | Promise<void>
137
+ 'render:html:close': (closeContext: NuxtRenderCloseContext, context: { event: H3Event }) => void | Promise<void>
138
+ 'render:route': (renderRouteContext: NuxtRenderRouteContext, context: { event: H3Event }) => void | Promise<void>
134
139
  'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
135
140
  }
136
141
  }
@@ -142,12 +147,14 @@ declare module 'nitropack/types' {
142
147
  interface NitroRuntimeConfig extends RuntimeConfig {}
143
148
  interface NitroRouteConfig {
144
149
  ssr?: boolean
150
+ streaming?: boolean
145
151
  noScripts?: boolean
146
152
  /** @deprecated Use \`noScripts\` instead */
147
153
  experimentalNoScripts?: boolean
148
154
  }
149
155
  interface NitroRouteRules {
150
156
  ssr?: boolean
157
+ streaming?: boolean
151
158
  noScripts?: boolean
152
159
  /** @deprecated Use \`noScripts\` instead */
153
160
  experimentalNoScripts?: boolean
@@ -156,7 +163,10 @@ declare module 'nitropack/types' {
156
163
  }
157
164
  interface NitroRuntimeHooks {
158
165
  'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
159
- 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
166
+ 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event, streaming?: boolean }) => void | Promise<void>
167
+ 'render:html:chunk': (chunkContext: NuxtRenderChunkContext, context: { event: H3Event }) => void | Promise<void>
168
+ 'render:html:close': (closeContext: NuxtRenderCloseContext, context: { event: H3Event }) => void | Promise<void>
169
+ 'render:route': (renderRouteContext: NuxtRenderRouteContext, context: { event: H3Event }) => void | Promise<void>
160
170
  'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
161
171
  }
162
172
  }
@@ -292,6 +302,7 @@ async function bundle(nuxt) {
292
302
  "#internal/nuxt/app-config": () => nuxt.vfs["#build/app.config.mjs"]?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, "") || "",
293
303
  "#spa-template": async () => `export const template = ${JSON.stringify(await spaLoadingTemplate(nuxt))}`,
294
304
  "#internal/entry-chunk.mjs": () => `export const entryFileName = undefined`,
305
+ "#internal/streaming-iife-chunk.mjs": () => `export const iifeChunkFileName = undefined`,
295
306
  "#internal/nuxt/entry-ids.mjs": () => `export default []`,
296
307
  "#internal/nuxt/nitro-config.mjs": () => {
297
308
  const hasCachedRoutes = Object.values(nitro.options.routeRules).some((r) => r.isr || r.cache);
@@ -306,11 +317,16 @@ async function bundle(nuxt) {
306
317
  `export const NUXT_SHARED_DATA = ${!!nuxt.options.experimental.sharedPrerenderData}`,
307
318
  `export const NUXT_PAYLOAD_EXTRACTION = ${nuxt.options.experimental.payloadExtraction !== false}`,
308
319
  `export const NUXT_PAYLOAD_INLINE = ${nuxt.options.experimental.payloadExtraction !== true}`,
309
- `export const NUXT_RUNTIME_PAYLOAD_EXTRACTION = ${hasCachedRoutes}`
320
+ `export const NUXT_RUNTIME_PAYLOAD_EXTRACTION = ${hasCachedRoutes}`,
321
+ `export const NUXT_SSR_STREAMING = ${!!(typeof nuxt.options.experimental.ssrStreaming === "object" && nuxt.options.experimental.ssrStreaming.enabled)}`,
322
+ `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) : "/^$/"}`
310
323
  ].join("\n");
311
324
  }
312
325
  },
313
- routeRules: { "/__nuxt_error": { cache: false } },
326
+ routeRules: {
327
+ ...typeof nuxt.options.experimental.ssrStreaming === "object" && nuxt.options.experimental.ssrStreaming.enabled ? { "/**": { streaming: true } } : {},
328
+ "/__nuxt_error": { cache: false }
329
+ },
314
330
  appConfig: nuxt.options.appConfig,
315
331
  appConfigFiles: layerDirs.map((dirs) => join(dirs.app, "app.config")),
316
332
  typescript: {
@@ -1,26 +1,31 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { getPrefetchLinks, getPreloadLinks, getRequestDependencies, renderResourceHeaders } from "vue-bundle-renderer/runtime";
3
- import { appendResponseHeader, createError, getQuery, getResponseStatus, getResponseStatusText, writeEarlyHints } from "h3";
3
+ import { renderToWebStream } from "vue/server-renderer";
4
+ import { appendResponseHeader, createError, getQuery, getRequestHeader, getResponseStatus, getResponseStatusText, removeResponseHeader, setResponseHeader, writeEarlyHints } from "h3";
4
5
  import { getQuery as getURLQuery, joinURL } from "ufo";
5
6
  import { propsToString, renderSSRHead } from "@unhead/vue/server";
7
+ import { createBootstrapScript, renderSSRHeadSuspenseChunk, renderShell } from "@unhead/vue/stream/server";
8
+ import { streamingIifeCode } from "@unhead/vue/stream/iife";
6
9
  import destr from "destr";
7
10
  import { defineRenderHandler, getRouteRules, useNitroApp } from "nitropack/runtime";
8
- import { getRenderer } from "../utils/renderer/build-files.mjs";
11
+ import { APP_ROOT_CLOSE_TAG, APP_ROOT_OPEN_TAG, getRenderer, getServerApp } from "../utils/renderer/build-files.mjs";
9
12
  import { payloadCache, prerenderRenderingURLs } from "../utils/cache.mjs";
10
13
  import { renderPayloadJsonScript, renderPayloadResponse, renderPayloadScript, splitPayload } from "../utils/renderer/payload.mjs";
11
14
  import { createSSRContext, setSSRError } from "../utils/renderer/app.mjs";
12
15
  import { renderInlineStyles } from "../utils/renderer/inline-styles.mjs";
13
- import { replaceIslandTeleports } from "../utils/renderer/islands.mjs";
16
+ import { renderStreamedIslandTeleports, replaceIslandTeleports } from "../utils/renderer/islands.mjs";
14
17
  // @ts-expect-error virtual file
15
18
  import { renderSSRHeadOptions } from "#internal/unhead.config.mjs";
16
19
  // @ts-expect-error virtual file
17
- import { NUXT_ASYNC_CONTEXT, NUXT_EARLY_HINTS, NUXT_INLINE_STYLES, NUXT_JSON_PAYLOADS, NUXT_NO_SCRIPTS, NUXT_PAYLOAD_EXTRACTION, NUXT_PAYLOAD_INLINE, NUXT_RUNTIME_PAYLOAD_EXTRACTION, PARSE_ERROR_DATA } from "#internal/nuxt/nitro-config.mjs";
20
+ import { NUXT_ASYNC_CONTEXT, NUXT_EARLY_HINTS, NUXT_INLINE_STYLES, NUXT_JSON_PAYLOADS, 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";
18
21
  // @ts-expect-error virtual file
19
- import { appHead, appTeleportAttrs, appTeleportTag, componentIslands } from "#internal/nuxt.config.mjs";
22
+ import { appHead, appTeleportAttrs, appTeleportTag, componentIslands, componentIslandsActive } from "#internal/nuxt.config.mjs";
20
23
  // @ts-expect-error virtual file
21
24
  import entryIds from "#internal/nuxt/entry-ids.mjs";
22
25
  // @ts-expect-error virtual file
23
26
  import { entryFileName } from "#internal/entry-chunk.mjs";
27
+ // @ts-expect-error virtual file
28
+ import { iifeChunkFileName } from "#internal/streaming-iife-chunk.mjs";
24
29
  import { buildAssetsURL, publicAssetsURL } from "../utils/paths.mjs";
25
30
  import { relative } from "pathe";
26
31
  // @ts-expect-error private property consumed by vite-generated url helpers
@@ -37,6 +42,8 @@ const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : "";
37
42
  const PAYLOAD_URL_RE = NUXT_JSON_PAYLOADS ? /^[^?]*\/_payload.json(?:\?.*)?$/ : /^[^?]*\/_payload.js(?:\?.*)?$/;
38
43
  const PAYLOAD_FILENAME = NUXT_JSON_PAYLOADS ? "_payload.json" : "_payload.js";
39
44
  let entryPath;
45
+
46
+ const SSR_BOT_RE = NUXT_SSR_STREAMING_BOT_RE;
40
47
  const handler = defineRenderHandler((event) => {
41
48
 
42
49
  const ssrError = event.path.startsWith("/__nuxt_error") ? getQuery(event) : null;
@@ -119,6 +126,27 @@ async function renderRoute(event, ssrError) {
119
126
  ssrContext.modules.add(id);
120
127
  }
121
128
  }
129
+
130
+
131
+
132
+ 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;
133
+ const renderRouteContext = {
134
+ canStream,
135
+ prefersStream: !!(NUXT_SSR_STREAMING && !SSR_BOT_RE.test(getRequestHeader(event, "user-agent") || ""))
136
+ };
137
+ await nitroApp.hooks.callHook("render:route", renderRouteContext, { event });
138
+ if (NUXT_SSR_STREAMING && canStream && renderRouteContext.prefersStream) {
139
+ return renderStreamedResponse({
140
+ event,
141
+ ssrContext,
142
+ renderer,
143
+ routeOptions,
144
+ ssrError,
145
+ _PAYLOAD_EXTRACTION,
146
+ _PAYLOAD_INLINE,
147
+ payloadURL
148
+ });
149
+ }
122
150
  const _rendered = await renderer.renderToString(ssrContext).catch(async (error) => {
123
151
 
124
152
 
@@ -299,6 +327,439 @@ async function renderRoute(event, ssrError) {
299
327
  };
300
328
  }
301
329
  export default handler;
330
+ async function renderStreamedResponse(ctx) {
331
+ const { event, ssrContext, renderer, routeOptions, ssrError, _PAYLOAD_EXTRACTION, _PAYLOAD_INLINE, payloadURL } = ctx;
332
+ const nitroApp = useNitroApp();
333
+ const NO_SCRIPTS = NUXT_NO_SCRIPTS || !!routeOptions?.noScripts;
334
+
335
+ const { link: linkHeader } = renderResourceHeaders({}, renderer.rendererContext);
336
+ if (linkHeader) {
337
+ appendResponseHeader(event, "link", linkHeader);
338
+ }
339
+
340
+ const entryInlineStyles = NUXT_INLINE_STYLES ? await renderInlineStyles(new Set(entryIds)) : [];
341
+
342
+ if (entryInlineStyles.length) {
343
+ ssrContext.head.push({ style: entryInlineStyles });
344
+ }
345
+
346
+ const { styles: entryStyles, scripts: entryScripts } = getRequestDependencies({}, renderer.rendererContext);
347
+ const shellLinks = [];
348
+ for (const resource of Object.values(entryStyles)) {
349
+ if (import.meta.dev && "inline" in getURLQuery(resource.file)) {
350
+ continue;
351
+ }
352
+ shellLinks.push({
353
+ rel: "stylesheet",
354
+ href: renderer.rendererContext.buildAssetsURL(resource.file),
355
+ crossorigin: ""
356
+ });
357
+ }
358
+ if (shellLinks.length) {
359
+ ssrContext.head.push({ link: shellLinks });
360
+ }
361
+
362
+ if (entryFileName && !NO_SCRIPTS) {
363
+ let path = entryPath;
364
+ if (!path) {
365
+ path = buildAssetsURL(entryFileName);
366
+ if (ssrContext.runtimeConfig.app.cdnURL || /^(?:\/|\.+\/)/.test(path)) {
367
+ entryPath = path;
368
+ } else {
369
+ path = relative(event.path.replace(/\/[^/]+$/, "/"), joinURL("/", path));
370
+ if (!/^(?:\/|\.+\/)/.test(path)) {
371
+ path = `./${path}`;
372
+ }
373
+ }
374
+ }
375
+ ssrContext.head.push({ script: [{
376
+ tagPosition: "head",
377
+ tagPriority: "critical",
378
+ type: "importmap",
379
+
380
+ innerHTML: { imports: { "#entry": path } }
381
+ }] });
382
+ }
383
+
384
+ if (_PAYLOAD_EXTRACTION && !_PAYLOAD_INLINE && !NO_SCRIPTS) {
385
+ ssrContext.head.push({ link: [NUXT_JSON_PAYLOADS ? {
386
+ rel: "preload",
387
+ as: "fetch",
388
+ crossorigin: "anonymous",
389
+ href: payloadURL
390
+ } : {
391
+ rel: "modulepreload",
392
+ crossorigin: "",
393
+ href: payloadURL
394
+ }] });
395
+ }
396
+
397
+ if (!NO_SCRIPTS) {
398
+ ssrContext.head.push({ link: getPreloadLinks({}, renderer.rendererContext) });
399
+ ssrContext.head.push({ link: getPrefetchLinks({}, renderer.rendererContext) });
400
+ }
401
+
402
+ if (!NO_SCRIPTS) {
403
+ ssrContext.head.push({ script: Object.values(entryScripts).map((resource) => ({
404
+ type: resource.module ? "module" : null,
405
+ src: renderer.rendererContext.buildAssetsURL(resource.file),
406
+ defer: resource.module ? null : true,
407
+ tagPosition: "head",
408
+ crossorigin: ""
409
+ })) });
410
+ }
411
+
412
+ if (!NO_SCRIPTS && !import.meta.dev && iifeChunkFileName) {
413
+ ssrContext.head.push({ link: [{
414
+ rel: "preload",
415
+ as: "script",
416
+ href: buildAssetsURL(iifeChunkFileName)
417
+ }] });
418
+ }
419
+
420
+
421
+
422
+
423
+
424
+ const createSSRApp = await getServerApp();
425
+ let vueApp;
426
+ try {
427
+ vueApp = await createSSRApp(ssrContext);
428
+ } catch (error) {
429
+
430
+ if ((ssrContext["~renderResponse"] || ssrContext._renderResponse) && error?.message === "skipping render") {
431
+
432
+
433
+ removeResponseHeader(event, "link");
434
+ return ssrContext["~renderResponse"] || ssrContext._renderResponse;
435
+ }
436
+ await ssrContext.nuxt?.hooks.callHook("app:error", error);
437
+ throw error;
438
+ }
439
+ if (ssrContext["~renderResponse"] || ssrContext._renderResponse) {
440
+ removeResponseHeader(event, "link");
441
+ return ssrContext["~renderResponse"] || ssrContext._renderResponse;
442
+ }
443
+
444
+
445
+ const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = renderShell(ssrContext.head);
446
+
447
+
448
+
449
+
450
+
451
+ const cspNonce = headTags.match(/<script[^>]+\bnonce="([^"]*)"/)?.[1];
452
+ const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
453
+
454
+
455
+
456
+
457
+
458
+ const bootstrapScript = NO_SCRIPTS ? "" : createBootstrapScript(undefined, cspNonce);
459
+ let iifeScript = "";
460
+ if (!NO_SCRIPTS) {
461
+ if (!import.meta.dev && iifeChunkFileName) {
462
+ iifeScript = `<script async${nonceAttr} src="${buildAssetsURL(iifeChunkFileName)}"><\/script>`;
463
+ } else {
464
+ iifeScript = `<script${nonceAttr}>${streamingIifeCode}<\/script>`;
465
+ }
466
+ }
467
+ const shellContext = {
468
+ htmlAttrs: htmlAttrs ? [htmlAttrs] : [],
469
+ head: normalizeChunks([bootstrapScript, headTags]),
470
+ bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
471
+ bodyPrepend: normalizeChunks([iifeScript, bodyTagsOpen]),
472
+ body: [],
473
+ bodyAppend: []
474
+ };
475
+ if (import.meta.dev) {
476
+ const initialBodyLen = shellContext.body.length;
477
+ const initialAppendLen = shellContext.bodyAppend.length;
478
+ await nitroApp.hooks.callHook("render:html", shellContext, {
479
+ event,
480
+ streaming: true
481
+ });
482
+ if (shellContext.body.length !== initialBodyLen || shellContext.bodyAppend.length !== initialAppendLen) {
483
+ console.warn(`[nuxt] \`render:html\` mutated \`body\`/\`bodyAppend\` while streaming (${event.path}). These fields are silently dropped because the body is about to stream - use the \`render:html:close\` hook instead.`);
484
+ }
485
+ } else {
486
+ await nitroApp.hooks.callHook("render:html", shellContext, {
487
+ event,
488
+ streaming: true
489
+ });
490
+ }
491
+ const shellHtml = "<!DOCTYPE html>" + `<html${joinAttrs(shellContext.htmlAttrs)}>` + `<head>${joinTags(shellContext.head)}</head>` + `<body${joinAttrs(shellContext.bodyAttrs)}>` + joinTags(shellContext.bodyPrepend);
492
+
493
+ const vueStream = renderToWebStream(vueApp, ssrContext);
494
+ const reader = vueStream.getReader();
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+ let firstChunk;
505
+ try {
506
+ const { done, value } = await reader.read();
507
+ if (!done) {
508
+ firstChunk = value;
509
+ }
510
+ } catch (error) {
511
+ reader.releaseLock();
512
+ removeResponseHeader(event, "link");
513
+ if (ssrContext["~renderResponse"] || ssrContext._renderResponse) {
514
+ return ssrContext["~renderResponse"] || ssrContext._renderResponse;
515
+ }
516
+ const _err = !ssrError && ssrContext.payload?.error || error;
517
+ await ssrContext.nuxt?.hooks.callHook("app:error", _err);
518
+ throw _err;
519
+ }
520
+ if (ssrContext["~renderResponse"] || ssrContext._renderResponse) {
521
+ reader.cancel().catch(() => {});
522
+ removeResponseHeader(event, "link");
523
+ return ssrContext["~renderResponse"] || ssrContext._renderResponse;
524
+ }
525
+ if (ssrContext.payload?.error && !ssrError) {
526
+ reader.cancel().catch(() => {});
527
+ removeResponseHeader(event, "link");
528
+ throw ssrContext.payload.error;
529
+ }
530
+
531
+
532
+
533
+
534
+ const committedSnapshot = import.meta.dev ? {
535
+ status: getResponseStatus(event),
536
+ statusText: getResponseStatusText(event),
537
+ headers: snapshotResponseHeaders(event)
538
+ } : null;
539
+
540
+ const encoder = new TextEncoder();
541
+ let chunkIndex = 0;
542
+
543
+
544
+
545
+ const enqueueChunk = async (controller, chunk) => {
546
+ const chunkContext = {
547
+ chunk,
548
+ index: chunkIndex++
549
+ };
550
+ await nitroApp.hooks.callHook("render:html:chunk", chunkContext, { event });
551
+ controller.enqueue(chunkContext.chunk);
552
+ };
553
+
554
+
555
+
556
+
557
+
558
+
559
+
560
+ const emittedStyles = new Set(Object.values(entryStyles).map((r) => r.file));
561
+ const inlinedCss = new Set(entryInlineStyles.map((s) => String(s.innerHTML)));
562
+ const renderRouteStyles = async () => {
563
+ let tags = "";
564
+ if (NUXT_INLINE_STYLES) {
565
+ for (const style of await renderInlineStyles(ssrContext.modules ?? [])) {
566
+ const css = String(style.innerHTML);
567
+ if (!css || inlinedCss.has(css)) {
568
+ continue;
569
+ }
570
+ inlinedCss.add(css);
571
+ tags += `<style${nonceAttr}>${css}</style>`;
572
+ }
573
+ return tags;
574
+ }
575
+ for (const resource of Object.values(getRequestDependencies(ssrContext, renderer.rendererContext).styles)) {
576
+ if (emittedStyles.has(resource.file)) {
577
+ continue;
578
+ }
579
+ if (import.meta.dev && "inline" in getURLQuery(resource.file)) {
580
+ continue;
581
+ }
582
+ emittedStyles.add(resource.file);
583
+ tags += `<link rel="stylesheet" crossorigin href="${renderer.rendererContext.buildAssetsURL(resource.file)}">`;
584
+ }
585
+ return tags;
586
+ };
587
+ const outputStream = new ReadableStream({
588
+ async start(controller) {
589
+ try {
590
+
591
+
592
+
593
+ await enqueueChunk(controller, encoder.encode(shellHtml));
594
+ await enqueueChunk(controller, encoder.encode(await renderRouteStyles() + APP_ROOT_OPEN_TAG));
595
+ if (firstChunk) {
596
+ await enqueueChunk(controller, firstChunk);
597
+ const headChunk = renderSSRHeadSuspenseChunk(ssrContext.head);
598
+ if (headChunk && !NO_SCRIPTS) {
599
+ await enqueueChunk(controller, encoder.encode(`<script${nonceAttr}>${headChunk};document.currentScript.remove()<\/script>`));
600
+ }
601
+ }
602
+
603
+ try {
604
+ while (true) {
605
+ const { done, value } = await reader.read();
606
+ if (done) {
607
+ break;
608
+ }
609
+ await enqueueChunk(controller, value);
610
+
611
+ const headChunk = renderSSRHeadSuspenseChunk(ssrContext.head);
612
+ if (headChunk && !NO_SCRIPTS) {
613
+ await enqueueChunk(controller, encoder.encode(`<script${nonceAttr}>${headChunk};document.currentScript.remove()<\/script>`));
614
+ }
615
+ }
616
+ } finally {
617
+ reader.releaseLock();
618
+ }
619
+
620
+
621
+
622
+ if (!NO_SCRIPTS) {
623
+ const finalHeadChunk = renderSSRHeadSuspenseChunk(ssrContext.head);
624
+ if (finalHeadChunk) {
625
+ await enqueueChunk(controller, encoder.encode(`<script${nonceAttr}>${finalHeadChunk};document.currentScript.remove()<\/script>`));
626
+ }
627
+ }
628
+
629
+ await ssrContext.nuxt?.hooks.callHook("app:rendered", {
630
+ ssrContext,
631
+ renderResult: {}
632
+ });
633
+
634
+
635
+
636
+ if (ssrContext.payload?.error && !ssrError) {
637
+ await ssrContext.nuxt?.hooks.callHook("app:error", ssrContext.payload.error);
638
+ }
639
+
640
+ if (!NO_SCRIPTS) {
641
+ ssrContext.head.push({ script: _PAYLOAD_INLINE ? NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({
642
+ ssrContext,
643
+ data: ssrContext.payload
644
+ }) : renderPayloadScript({
645
+ ssrContext,
646
+ data: ssrContext.payload,
647
+ routeOptions
648
+ }) : NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({
649
+ ssrContext,
650
+ data: splitPayload(ssrContext).initial,
651
+ src: payloadURL
652
+ }) : renderPayloadScript({
653
+ ssrContext,
654
+ data: splitPayload(ssrContext).initial,
655
+ routeOptions,
656
+ src: payloadURL
657
+ }) }, {
658
+ tagPosition: "bodyClose",
659
+ tagPriority: "high"
660
+ });
661
+ }
662
+
663
+
664
+
665
+ const closingHead = applyRenderOptions(ssrContext.head.render(), renderSSRHeadOptions);
666
+ const closeContext = { bodyAppend: normalizeChunks([bodyTags, closingHead.bodyTags]) };
667
+ await nitroApp.hooks.callHook("render:html:close", closeContext, { event });
668
+
669
+
670
+
671
+
672
+
673
+ const teleportHtml = APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportAttrs.id}`]]) : "") + APP_TELEPORT_CLOSE_TAG;
674
+
675
+
676
+
677
+
678
+
679
+ const islandTeleports = NO_SCRIPTS ? "" : renderStreamedIslandTeleports(ssrContext, nonceAttr);
680
+ const closingHtml = APP_ROOT_CLOSE_TAG + await renderRouteStyles() + teleportHtml + (ssrContext.teleports?.body || "") + islandTeleports + joinTags(closeContext.bodyAppend) + "</body></html>";
681
+ await enqueueChunk(controller, encoder.encode(closingHtml));
682
+ controller.close();
683
+ if (committedSnapshot) {
684
+ const currentStatus = getResponseStatus(event);
685
+ const currentStatusText = getResponseStatusText(event);
686
+ const currentHeaders = snapshotResponseHeaders(event);
687
+ const lateMutations = [];
688
+ if (currentStatus !== committedSnapshot.status) {
689
+ lateMutations.push(`response status changed from ${committedSnapshot.status || 200} to ${currentStatus} (e.g. \`setResponseStatus\`)`);
690
+ }
691
+ if (currentStatusText !== committedSnapshot.statusText) {
692
+ lateMutations.push(`response statusText changed (e.g. \`setResponseStatus\`)`);
693
+ }
694
+ if (currentHeaders !== committedSnapshot.headers) {
695
+ lateMutations.push(`response headers changed during render (e.g. \`useCookie\`, \`useResponseHeader\`, \`setHeader\`)`);
696
+ }
697
+ if (lateMutations.length) {
698
+ 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.path}\n Move the mutation into a plugin (which runs before the shell is flushed), or opt this route out of streaming with \`routeRules: { '${event.path}': { streaming: false } }\` or the \`render:route\` hook.`);
699
+ }
700
+ }
701
+ } catch (error) {
702
+
703
+
704
+
705
+
706
+
707
+ await Promise.resolve(ssrContext.nuxt?.hooks.callHook("app:error", error)).catch(() => {});
708
+ ssrContext.payload ||= {};
709
+ ssrContext.payload.error ||= error;
710
+ try {
711
+ if (!NO_SCRIPTS) {
712
+ ssrContext.head.push({ script: NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({
713
+ ssrContext,
714
+ data: ssrContext.payload
715
+ }) : renderPayloadScript({
716
+ ssrContext,
717
+ data: ssrContext.payload,
718
+ routeOptions
719
+ }) }, {
720
+ tagPosition: "bodyClose",
721
+ tagPriority: "high"
722
+ });
723
+ const tail = applyRenderOptions(ssrContext.head.render(), renderSSRHeadOptions);
724
+ controller.enqueue(encoder.encode(tail.bodyTags));
725
+ }
726
+ } catch {}
727
+ controller.enqueue(encoder.encode(APP_ROOT_CLOSE_TAG + "</body></html>"));
728
+ controller.close();
729
+ }
730
+ },
731
+ cancel(reason) {
732
+
733
+
734
+
735
+ reader.cancel(reason).catch(() => {});
736
+ }
737
+ });
738
+ setResponseHeader(event, "content-type", "text/html;charset=utf-8");
739
+ setResponseHeader(event, "x-powered-by", "Nuxt");
740
+ return { body: outputStream };
741
+ }
742
+
743
+
744
+
745
+
746
+
747
+ function applyRenderOptions(payload, options) {
748
+ if (!options.omitLineBreaks) {
749
+ return payload;
750
+ }
751
+ return {
752
+ headTags: payload.headTags.replaceAll("\n", ""),
753
+ bodyTags: payload.bodyTags.replaceAll("\n", ""),
754
+ bodyTagsOpen: payload.bodyTagsOpen.replaceAll("\n", ""),
755
+ htmlAttrs: payload.htmlAttrs,
756
+ bodyAttrs: payload.bodyAttrs
757
+ };
758
+ }
759
+ function snapshotResponseHeaders(event) {
760
+ const headers = event.node.res.getHeaders();
761
+ return Object.entries(headers).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : v}`).sort().join("\n");
762
+ }
302
763
  function normalizeChunks(chunks) {
303
764
  const result = [];
304
765
  for (const _chunk of chunks) {
@@ -1,5 +1,9 @@
1
1
  import type { RendererContext } from "vue-bundle-renderer/runtime";
2
+ import type { App } from "vue";
2
3
  import type { NuxtSSRContext } from "nuxt/app";
4
+ type Entry = (ssrContext?: NuxtSSRContext) => Promise<App>;
5
+ export declare const APP_ROOT_OPEN_TAG: string;
6
+ export declare const APP_ROOT_CLOSE_TAG: string;
3
7
  interface Renderer {
4
8
  rendererContext: RendererContext;
5
9
  renderToString(ssrContext: NuxtSSRContext): Promise<{
@@ -12,5 +16,6 @@ interface Renderer {
12
16
  }
13
17
  export declare const getSSRRenderer: () => Promise<Renderer>;
14
18
  export declare function getRenderer(ssrContext: NuxtSSRContext): Promise<Renderer>;
19
+ export declare const getServerApp: () => Promise<Entry>;
15
20
  export declare const getSSRStyles: () => Promise<Record<string, () => Promise<string[]>>>;
16
21
  export {};
@@ -11,8 +11,8 @@ import { buildAssetsURL, publicAssetsURL } from "../paths.mjs";
11
11
  globalThis.__buildAssetsURL = buildAssetsURL;
12
12
  // @ts-expect-error private property consumed by vite-generated url helpers
13
13
  globalThis.__publicAssetsURL = publicAssetsURL;
14
- const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`;
15
- const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`;
14
+ export const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`;
15
+ export const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`;
16
16
  // @ts-expect-error file will be produced after app build
17
17
  const getServerEntry = () => import("#build/dist/server/server.mjs").then((r) => r.default || r);
18
18
  // @ts-expect-error file will be produced after app build
@@ -99,5 +99,7 @@ function lazyCachedFunction(fn) {
99
99
  export function getRenderer(ssrContext) {
100
100
  return NUXT_NO_SSR || ssrContext.noSSR ? getSPARenderer() : getSSRRenderer();
101
101
  }
102
+
103
+ export const getServerApp = lazyCachedFunction(getServerEntry);
102
104
  // @ts-expect-error file will be produced after app build
103
105
  export const getSSRStyles = lazyCachedFunction(() => import("#build/dist/server/styles.mjs").then((r) => r.default || r));
@@ -6,4 +6,10 @@ export declare function getServerComponentHTML(body: string): string;
6
6
  export declare function getSlotIslandResponse(ssrContext: NuxtSSRContext): NuxtIslandResponse["slots"];
7
7
  export declare function getClientIslandResponse(ssrContext: NuxtSSRContext): NuxtIslandResponse["components"];
8
8
  export declare function getComponentSlotTeleport(clientUid: string, teleports: Record<string, string>): Record<string, string>;
9
+ /**
10
+ * Emit each island teleport as an inert `<template>` keyed by its anchor,
11
+ * followed by a relocation script that moves the content into place before
12
+ * hydration. Returns an empty string when there are no island teleports.
13
+ */
14
+ export declare function renderStreamedIslandTeleports(ssrContext: NuxtSSRContext, nonceAttr?: string): string;
9
15
  export declare function replaceIslandTeleports(ssrContext: NuxtSSRContext, html: string): string;
@@ -53,6 +53,43 @@ export function getComponentSlotTeleport(clientUid, teleports) {
53
53
  }
54
54
  return slots;
55
55
  }
56
+
57
+
58
+
59
+
60
+
61
+ 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()}})()`;
62
+
63
+ export function renderStreamedIslandTeleports(ssrContext, nonceAttr = "") {
64
+ const { teleports, islandContext } = ssrContext;
65
+ if (islandContext || !teleports) {
66
+ return "";
67
+ }
68
+ let templates = "";
69
+ for (const key in teleports) {
70
+ const matchClientComp = key.match(SSR_CLIENT_TELEPORT_MARKER);
71
+ if (matchClientComp) {
72
+ const [, uid, clientId] = matchClientComp;
73
+ if (!uid || !clientId) {
74
+ continue;
75
+ }
76
+ templates += `<template data-island-uid="${uid}" data-island-component="${clientId}">${teleports[key]}</template>`;
77
+ continue;
78
+ }
79
+ const matchSlot = key.match(SSR_SLOT_TELEPORT_MARKER);
80
+ if (matchSlot) {
81
+ const [, uid, slot] = matchSlot;
82
+ if (!uid || !slot) {
83
+ continue;
84
+ }
85
+ templates += `<template data-island-uid="${uid}" data-island-slot="${slot}">${teleports[key]}</template>`;
86
+ }
87
+ }
88
+ if (!templates) {
89
+ return "";
90
+ }
91
+ return templates + `<script${nonceAttr}>${ISLAND_TELEPORT_RELOCATE_SCRIPT}<\/script>`;
92
+ }
56
93
  export function replaceIslandTeleports(ssrContext, html) {
57
94
  const { teleports, islandContext } = ssrContext;
58
95
  if (islandContext || !teleports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt/nitro-server-nightly",
3
- "version": "4.5.0-29673761.f253a70d",
3
+ "version": "4.5.0-29673775.d239dbcd",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nuxt/nuxt.git",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "@nuxt/devalue": "^2.0.2",
30
- "@nuxt/kit": "npm:@nuxt/kit-nightly@4.5.0-29673761.f253a70d",
30
+ "@nuxt/kit": "npm:@nuxt/kit-nightly@4.5.0-29673775.d239dbcd",
31
31
  "@unhead/vue": "^3.1.0",
32
32
  "@vue/shared": "^3.5.35",
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@4.5.0-29673761.f253a70d"
61
+ "nuxt": "npm:nuxt-nightly@4.5.0-29673775.d239dbcd"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@babel/plugin-proposal-decorators": {
@@ -74,9 +74,9 @@
74
74
  "devDependencies": {
75
75
  "@babel/plugin-proposal-decorators": "7.29.7",
76
76
  "@babel/plugin-syntax-typescript": "7.29.7",
77
- "@nuxt/schema": "npm:@nuxt/schema-nightly@4.5.0-29673761.f253a70d",
77
+ "@nuxt/schema": "npm:@nuxt/schema-nightly@4.5.0-29673775.d239dbcd",
78
78
  "@rollup/plugin-babel": "7.1.0",
79
- "nuxt": "npm:nuxt-nightly@4.5.0-29673761.f253a70d",
79
+ "nuxt": "npm:nuxt-nightly@4.5.0-29673775.d239dbcd",
80
80
  "obuild": "0.4.36",
81
81
  "vitest": "4.1.7"
82
82
  },