vite-ssr-vue
Version:
Vite utility for vue3 server side rendering
214 lines (206 loc) • 6.06 kB
JavaScript
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 };