import devalue from '@nuxt/devalue' import { getRequestURL, setResponseHeader } from 'h3' import { renderToWebStream } from 'vue/server-renderer' import { renderSSRHead } from '@unhead/ssr' import { minify } from 'html-minifier-terser' import type { VueroServerRender, VueroSSRContext } from '/@server/types' import { createApp } from '/@src/app' const placeholderAppRe = /
<\/div>/ const placeholderStream = '' export const render: VueroServerRender = async ({ event, manifest, template, }) => { const url = getRequestURL(event) event.context.initialState = {} const { app, router, pinia, head } = await createApp(event) // set the router to the desired URL before rendering router.push(url.pathname) await router.isReady() // passing SSR context object which will be available via useSSRContext() // @vitejs/plugin-vue injects code into a component's setup() that registers // itself on ctx.modules. After the render, ctx.modules would contain all the // components that have been instantiated during this render call. const ctx: VueroSSRContext = { event, } const stream = renderToWebStream(app, ctx) const { headTags, htmlAttrs, bodyAttrs, bodyTags, bodyTagsOpen, } = await renderSSRHead(head) event.context.initialState.pinia = pinia?.state.value // the SSR manifest generated by Vite contains module -> chunk/asset mapping // which we can then use to determine what files need to be preloaded for this // request. const preloadLinks = renderPreloadLinks(ctx.modules, manifest) let wrapper = template .replace(``, ``) .replace(``, `${headTags}`) .replace(``, `${preloadLinks}`) .replace(``, `${bodyTagsOpen}`) .replace(``, `${bodyTags}`) // minify app wrapper wrapper = await minify(wrapper, { collapseWhitespace: true, collapseInlineTagWhitespace: true, removeComments: true, minifyJS: true, }) wrapper = wrapper.replace( placeholderAppRe, `
${placeholderStream}
`, ) setResponseHeader(event, 'Content-Type', 'text/html') return stream.pipeThrough(wrapTemplate(wrapper)) } function wrapTemplate(html: string) { const [pre, post] = html.split(placeholderStream) as [string, string] return new TransformStream({ start(controller) { controller.enqueue(new TextEncoder().encode(pre)) }, flush(controller) { controller.enqueue(new TextEncoder().encode(post)) }, }) } function renderPreloadLinks(modules?: any, manifest?: any) { let links = '' if (!modules) return links if (!manifest) return links const seen = new Set() modules?.forEach((id: string) => { const files = manifest[id] if (files) { files.forEach((file: any) => { if (!seen.has(file)) { seen.add(file) links += renderPreloadLink(file) } }) } }) return links } function renderPreloadLink(file: string) { if (file.endsWith('.js') || file.endsWith('.mjs')) { return `` } else if (file.endsWith('.css')) { return `` } else if (file.endsWith('.woff')) { return `` } else if (file.endsWith('.woff2')) { return `` } else if (file.endsWith('.gif')) { return `` } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) { return `` } else if (file.endsWith('.png')) { return `` } else if (file.endsWith('.webp')) { return `` } else if (file.endsWith('.svg')) { return `` } else { console.log('missing preload link for', file) return '' } }