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
JavaScript
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);
}