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 ''
}
}