From 409e8ebf37a71b68925331bd25dd44ef13d8983f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 7 Aug 2024 12:42:27 +0100 Subject: [PATCH] wip --- .../nuxt/src/core/runtime/nitro/renderer.ts | 207 +++++++++++++++++- playground/app.vue | 13 -- playground/components/TestMe.vue | 14 ++ playground/pages/index.vue | 24 ++ 4 files changed, 235 insertions(+), 23 deletions(-) delete mode 100644 playground/app.vue create mode 100644 playground/components/TestMe.vue create mode 100644 playground/pages/index.vue diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 9fea4d2761..74274f2cfc 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -1,21 +1,15 @@ import { AsyncLocalStorage } from 'node:async_hooks' -import { - createRenderer, - getPrefetchLinks, - getPreloadLinks, - getRequestDependencies, - renderResourceHeaders, -} from 'vue-bundle-renderer/runtime' +import { createRenderer, getPrefetchLinks, getPreloadLinks, getRequestDependencies, getResources, renderResourceHeaders } from 'vue-bundle-renderer/runtime' import type { Manifest as ClientManifest } from 'vue-bundle-renderer' -import type { RenderResponse } from 'nitro/types' +import type { NitroApp, RenderResponse } from 'nitro/types' import type { Manifest } from 'vite' import type { H3Event } from 'h3' -import { appendResponseHeader, createError, getQuery, getResponseStatus, getResponseStatusText, readBody, writeEarlyHints } from 'h3' +import { appendResponseHeader, createError, getHeader, getQuery, getResponseStatus, getResponseStatusText, readBody, writeEarlyHints } from 'h3' import devalue from '@nuxt/devalue' import { stringify, uneval } from 'devalue' import destr from 'destr' import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo' -import { renderToString as _renderToString } from 'vue/server-renderer' +import { renderToString as _renderToString, renderToWebStream } from 'vue/server-renderer' import { hash } from 'ohash' import { propsToString, renderSSRHead } from '@unhead/ssr' import type { HeadEntryOptions } from '@unhead/schema' @@ -339,6 +333,10 @@ export default defineRenderHandler(async (event): Promise { // We use error to bypass full render if we have an early response we can make if (ssrContext._renderResponse && error.message === 'skipping render') { return {} as ReturnType } @@ -710,3 +708,192 @@ function replaceIslandTeleports (ssrContext: NuxtSSRContext, html: string) { } return html } + +function renderStreamedHTMLDocument (html: NuxtRenderHTMLContext) { + return [ + '' + + `` + + `${joinTags(html.head)}` + + `${joinTags(html.bodyPrepend)}` + + APP_ROOT_OPEN_TAG, + // HTML body will be streamed here + APP_ROOT_CLOSE_TAG + + `` + + '', + ] +} +async function streamedResponse (event: H3Event, ssrContext: NuxtSSRContext, renderer: Awaited>, head: ReturnType, nitroApp: NitroApp) { + const createApp = await Promise.resolve(getServerEntry()).then(r => 'default' in r ? r.default : r) + + // Create a ReadableStream + const stream = new ReadableStream({ + start (controller) { + const render = async () => { + const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR + const routeOptions = getRouteRules(event) + const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts + const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.cdnURL || ssrContext.runtimeConfig.app.baseURL, event.path, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined + const inlinedStyles = (process.env.NUXT_INLINE_STYLES) ? await renderInlineStyles(ssrContext.modules ?? []) : [] + + // Setup head + const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext) + // 1.Extracted payload preloading + if (_PAYLOAD_EXTRACTION && !NO_SCRIPTS) { + head.push({ + link: [ + process.env.NUXT_JSON_PAYLOADS + ? { rel: 'preload', as: 'fetch', crossorigin: 'anonymous', href: payloadURL } + : { rel: 'modulepreload', href: payloadURL }, + ], + }, { mode: 'server' }) + } + + // 2. Styles + // cacheable + head.push({ style: inlinedStyles }) + const link: Link[] = [] + for (const style in styles) { + const resource = styles[style] + // Do not add links to resources that are inlined (vite v5+) + if (import.meta.dev && 'inline' in getURLQuery(resource.file)) { + continue + } + link.push({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) }) + } + head.push({ link }, { mode: 'server' }) + + // 5. Scripts + if (!routeOptions.experimentalNoScripts) { + head.push({ + script: Object.values(scripts).map(resource => ( - - - - diff --git a/playground/components/TestMe.vue b/playground/components/TestMe.vue new file mode 100644 index 0000000000..4aa55664b1 --- /dev/null +++ b/playground/components/TestMe.vue @@ -0,0 +1,14 @@ + + + + diff --git a/playground/pages/index.vue b/playground/pages/index.vue new file mode 100644 index 0000000000..6ec142a6af --- /dev/null +++ b/playground/pages/index.vue @@ -0,0 +1,24 @@ + + + + +