UNPKG

nuxt-resource-hints

Version:

Nuxt module that adds HTTP header resource hints for resources in your Nuxt App.

118 lines (117 loc) 4.8 kB
import { getResponseHeaders, setResponseHeaders, appendResponseHeader } from "h3"; import { defineNitroPlugin, useStorage } from "nitropack/runtime"; export default defineNitroPlugin(async (nitroApp) => { nitroApp.hooks.hook("render:html", (html, { event }) => { const link = generateLinkHeader(html.head, { resources: { stylesheet: true, preload: true, module_preload: true, prefetch: false, images: true, fonts: false, scripts: true, dns_prefetch: true, preconnect: true }, headerLength: 1990 }); if (link !== "") { appendResponseHeader(event, "link", link); } }); if (import.meta.prerender) { const preRenderedHeaders = {}; nitroApp.hooks.hook("render:html", (_html, { event }) => { const headers = getResponseHeaders(event); const path = event.path.split("?")[0]; preRenderedHeaders[path] = headers; }); nitroApp.hooks.hook("close", async () => { const headers = Object.fromEntries( Object.entries(preRenderedHeaders).map(([path, headers2]) => { const headersEntries = Object.entries(headers2).filter(([header]) => header === "link").map(([header, value]) => { if (Array.isArray(value)) { return [header, value.join(";")]; } else { return [header, value]; } }); return [path, Object.fromEntries(headersEntries)]; }) ); await useStorage("build:nuxt-resource-hints").setItem( "headers.json", headers ); }); } else { const preRenderedHeaders = await useStorage("assets:nuxt-resource-hints").getItem("headers.json") || {}; nitroApp.hooks.hook("beforeResponse", (event) => { const path = event.path.split("?")[0]; if (preRenderedHeaders[path]) { setResponseHeaders(event, preRenderedHeaders[path]); } }); } }); function generateLinkHeader(head, options) { const linkRegex = /<link\s([^>]+)>/g; const attrRegex = /\brel="(?<rel>[^"]+)"|\bhref="(?<href>[^"]+)"|\bas="(?<as>[^"]+)"|\b(?<crossoriginKey>crossorigin)(?:="(?<crossoriginValue>[^"]*)")?|\bfetchpriority="(?<fetchpriority>[^"]+)"|\bimagesrcset="(?<imagesrcset>[^"]+)"/gi; let linkHeader = ""; for (const headChildElem of head) { let match; while ((match = linkRegex.exec(headChildElem)) !== null) { const attributes = match[1]; const result = {}; for (const m of attributes.matchAll(attrRegex)) { for (const [key, value] of Object.entries(m.groups || {})) { if (value) result[key] = value; } } if (result.rel && result.href) { let includePreload = false; if (result.rel === "preload") { if (result.as === "script" && options.resources.scripts) { includePreload = true; } else if (result.as === "font" && options.resources.fonts) { includePreload = true; } else if (result.as === "image" && options.resources.images) { includePreload = true; if (result.imagesrcset && isSrcsetSame(result.href, result.imagesrcset)) { result.imagesrcset = void 0; } } else if (result.as === "style" && options.resources.stylesheet) { includePreload = true; } } const includeResource = options.resources.stylesheet && result.rel === "stylesheet" || includePreload || options.resources.module_preload && result.rel === "modulepreload" || options.resources.prefetch && result.rel === "prefetch" || options.resources.dns_prefetch && result.rel === "dns-prefetch" || options.resources.preconnect && result.rel === "preconnect"; if (includeResource) { let blocking = false; if (result.rel === "stylesheet") { result.as = "style"; result.rel = "preload"; blocking = true; } const link = `<${result.href}>; rel="${result.rel}"${result.as ? `; as="${result.as}"` : ""}${result.crossoriginKey ? `; crossorigin${!result.crossoriginValue || result.crossoriginValue === "anonymous" ? "" : `="${result.crossoriginValue}"`}` : ""}${result.imagesrcset ? `; imagesrcset="${result.imagesrcset}"` : ""}${result.fetchpriority ? `; fetchpriority="${result.fetchpriority}"` : ""}${blocking ? "; blocking" : ""}`; if (linkHeader.length + link.length + 2 >= options.headerLength) { return linkHeader; } linkHeader += linkHeader ? `, ${link}` : link; } } } } return linkHeader; } function isSrcsetSame(href, srcset) { const sources = srcset.split(",").map((s) => s.trim().split(/\s+/)[0]); return sources.every((url) => url === href); }