Files
dmz/src/entry-server.ts
2025-05-24 01:47:40 +09:00

145 lines
4.3 KiB
TypeScript

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 id="app"([\s\w\-"'=[\]]*)><\/div>/
const placeholderStream = '<!--vueroplaceholder-->'
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(`<html>`, `<html${htmlAttrs}>`)
.replace(`<head>`, `<head>${headTags}`)
.replace(`</head>`, `${preloadLinks}</head>`)
.replace(`<body>`, `<body${bodyAttrs}>${bodyTagsOpen}`)
.replace(`</body>`, `${bodyTags}</body>`)
// minify app wrapper
wrapper = await minify(wrapper, {
collapseWhitespace: true,
collapseInlineTagWhitespace: true,
removeComments: true,
minifyJS: true,
})
wrapper = wrapper.replace(
placeholderAppRe,
`<div id="app" data-server-rendered="true"$1>${placeholderStream}</div><script>window.__vuero__=${devalue(
event.context.initialState,
)}</script>`,
)
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<Uint8Array, Uint8Array>({
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 `<link rel="modulepreload" crossorigin href="${file}">`
}
else if (file.endsWith('.css')) {
return `<link rel="stylesheet" href="${file}">`
}
else if (file.endsWith('.woff')) {
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
}
else if (file.endsWith('.woff2')) {
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
}
else if (file.endsWith('.gif')) {
return `<link rel="preload" href="${file}" as="image" type="image/gif">`
}
else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
return `<link rel="preload" href="${file}" as="image" type="image/jpeg">`
}
else if (file.endsWith('.png')) {
return `<link rel="preload" href="${file}" as="image" type="image/png">`
}
else if (file.endsWith('.webp')) {
return `<link rel="preload" href="${file}" as="image" type="image/webp">`
}
else if (file.endsWith('.svg')) {
return `<link rel="prefetch" href="${file}" as="image" type="image/svg+xml"/>`
}
else {
console.log('missing preload link for', file)
return ''
}
}