UNPKG

vite-ssr-vue

Version:

Vite utility for vue3 server side rendering

214 lines (206 loc) 6.06 kB
import { ref, onMounted, createSSRApp } from 'vue'; import { renderToString } from '@vue/server-renderer'; import { renderHeadToString } from '@vueuse/head'; import { parse } from 'node-html-parser'; const UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; const ESCAPED_CHARS = { "<": "\\u003C", ">": "\\u003E", "/": "\\u002F", "\u2028": "\\u2028", "\u2029": "\\u2029" }; const escape = (unsafeChar) => { return ESCAPED_CHARS[unsafeChar]; }; const serialize = (state) => { try { return JSON.stringify(JSON.stringify(state || {})).replace( UNSAFE_CHARS_REGEXP, escape ); } catch (e) { throw new Error(`[SSR] On state serialization - ${e.message}`); } }; const createUrl = (url) => { url = url || "/"; if (url instanceof URL) { return url; } if (!(url || "").includes("://")) { url = "http://e.g" + (url.startsWith("/") ? url : `/${url}`); } return new URL(url); }; const ClientOnly = { name: "ClientOnly", setup(props, { slots }) { const show = ref(false); onMounted(() => { show.value = true; }); return () => show.value && slots.default ? slots.default() : null; } }; const fileType = (file) => { const ext = file.split(".").pop()?.toLowerCase() || ""; if (ext === "js") { return "script"; } else if (ext === "css") { return "style"; } else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) { return "image"; } else if (/woff2?|ttf|otf|eot/.test(ext)) { return "font"; } return ""; }; const findDependencies = (modules, manifest, shouldPreload, shouldPrefetch) => { const preload = /* @__PURE__ */ new Set(); const prefetch = /* @__PURE__ */ new Set(); for (const id of modules || []) { for (const file of manifest[id] || []) { const asType = fileType(file); if (!shouldPreload && asType !== "script" && asType !== "style") { continue; } if (typeof shouldPreload === "function" && !shouldPreload(file, asType)) { continue; } preload.add(file); } } for (const id of Object.keys(manifest)) { for (const file of manifest[id]) { if (!preload.has(file)) { const asType = fileType(file); if (!shouldPrefetch) { continue; } if (shouldPrefetch && !shouldPrefetch(file, asType)) { continue; } prefetch.add(file); } } } return { preload: [...preload], prefetch: [...prefetch] }; }; const renderPreloadLinks = (files) => { const link = []; for (const file of files || []) { const asType = fileType(file); const ext = file.split(".").pop()?.toLowerCase() || ""; if (asType === "script") { link.push(`<link rel="modulepreload" crossorigin href="${file}">`); } else if (asType === "style") { link.push(`<link rel="stylesheet" href="${file}">`); } else if (asType === "font") { link.push(`<link rel="stylesheet" href="${file}" type="font/${ext}" crossorigin>`); } else { link.push(`<link rel="stylesheet" href="${file}">`); } } return link; }; const renderPrefetchLinks = (files) => { const link = []; for (const file of files || []) { link.push(`<link rel="prefetch" href="${file}">`); } return link; }; const teleportsInject = (body, teleports = {}) => { const teleportsKeys = Object.keys(teleports); if (teleportsKeys.length) { const root = parse(body, { comment: true }); teleportsKeys.map((key) => { const el = root.querySelector(key); if (el) { if (el.childNodes) { el.childNodes.unshift(parse(teleports[key], { comment: true })); } else { el.appendChild(parse(teleports[key], { comment: true })); } } }); return root.toString(); } return body; }; const createViteSsrVue = (App, options = {}) => { return async (url, { manifest, ...extra } = {}) => { const app = createSSRApp(App, options.rootProps); const serializer = options.serializer || serialize; const ssrContext = { url: createUrl(url), isClient: false, initialState: {}, ...extra }; const { head, router, store, inserts, context, pinia } = options.created && await options.created({ app, ...ssrContext }) || {}; if (router && url) { await router.push(url); await router.isReady(); } options.mounted && await options.mounted({ app, router, store, pinia, ...ssrContext }); if (store) { ssrContext.initialState.state = store.state; } if (pinia) { ssrContext.initialState.pinia = pinia.state.value; } const body = inserts?.body || await renderToString(app, Object.assign(ssrContext, context || {})); let headTags = inserts?.headTags || "", htmlAttrs = inserts?.htmlAttrs || "", bodyAttrs = inserts?.bodyAttrs || "", dependencies = inserts?.dependencies || []; if (head) { ({ headTags, htmlAttrs, bodyAttrs } = await renderHeadToString(head)); } if (manifest) { const { preload, prefetch } = findDependencies( ssrContext.modules, manifest, options.shouldPreload, options.shouldPrefetch ); dependencies = preload; if (preload.length > 0) { const links = renderPreloadLinks(preload); headTags += links.length ? "\n" + links.join("\n") : ""; } if (prefetch.length > 0) { const links = renderPrefetchLinks(prefetch); headTags += links.length ? "\n" + links.join("\n") : ""; } } options.rendered && await options.rendered({ app, router, store, pinia, ...ssrContext }); const initialState = await serializer(ssrContext.initialState || {}); const teleports = ssrContext?.teleports || {}; return { html: teleportsInject(`__VITE_SSR_VUE_HTML__`, teleports), htmlAttrs, bodyAttrs, headTags, body, initialState, dependencies, teleports }; }; }; export { ClientOnly, createViteSsrVue as default };