@tanstack/start-server-core
Version:
Modern and scalable routing for React applications
254 lines (253 loc) • 8.09 kB
JavaScript
import { getScriptPreloadAttrs, getStylesheetHref, resolveManifestCssLink } from "@tanstack/router-core";
//#region src/early-hints.ts
var LINK_PARAM_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
var PRELOAD_AS_VALUES = new Set([
"fetch",
"font",
"image",
"script",
"style",
"track"
]);
function buildLinkParam(name, value) {
if (value === void 0) return name;
if (LINK_PARAM_TOKEN_RE.test(value)) return `${name}=${value}`;
return `${name}=${JSON.stringify(value)}`;
}
function serializeEarlyHint(hint) {
const parts = [`<${hint.href}>`, buildLinkParam("rel", hint.rel)];
if (hint.as) parts.push(buildLinkParam("as", hint.as));
if (hint.crossOrigin !== void 0) parts.push(buildLinkParam("crossorigin", hint.crossOrigin || void 0));
if (hint.type) parts.push(buildLinkParam("type", hint.type));
if (hint.integrity) parts.push(buildLinkParam("integrity", hint.integrity));
if (hint.referrerPolicy) parts.push(buildLinkParam("referrerpolicy", hint.referrerPolicy));
if (hint.fetchPriority) parts.push(buildLinkParam("fetchpriority", hint.fetchPriority));
return parts.join("; ");
}
function getStringAttr(attrs, name, fallbackName) {
const value = attrs?.[name] ?? (fallbackName ? attrs?.[fallbackName] : void 0);
return typeof value === "string" ? value : void 0;
}
function getPreloadAs(attrs) {
const as = getStringAttr(attrs, "as");
return as && PRELOAD_AS_VALUES.has(as) ? as : void 0;
}
function addEarlyHintFetchAttrs(hint, attrs) {
const crossOrigin = getStringAttr(attrs, "crossOrigin", "crossorigin");
const type = getStringAttr(attrs, "type");
const integrity = getStringAttr(attrs, "integrity");
const referrerPolicy = getStringAttr(attrs, "referrerPolicy", "referrerpolicy");
const fetchPriority = getStringAttr(attrs, "fetchPriority", "fetchpriority");
if (crossOrigin !== void 0) hint.crossOrigin = crossOrigin;
if (type) hint.type = type;
if (integrity) hint.integrity = integrity;
if (referrerPolicy) hint.referrerPolicy = referrerPolicy;
if (fetchPriority) hint.fetchPriority = fetchPriority;
}
function linkAttrsToEarlyHint(attrs) {
const href = getStringAttr(attrs, "href");
const rel = getStringAttr(attrs, "rel");
if (!href || !rel) return void 0;
const relTokens = rel.split(/\s+/);
let hintRel;
let hintAs;
if (relTokens.includes("modulepreload")) {
hintRel = "modulepreload";
hintAs = "script";
} else if (relTokens.includes("stylesheet")) {
hintRel = "preload";
hintAs = "style";
} else if (relTokens.includes("preload")) {
hintAs = getPreloadAs(attrs);
if (!hintAs) return void 0;
hintRel = "preload";
} else if (relTokens.includes("preconnect")) {
hintRel = "preconnect";
hintAs = void 0;
} else if (relTokens.includes("dns-prefetch")) {
hintRel = "dns-prefetch";
hintAs = void 0;
}
if (!hintRel) return void 0;
const hint = {
href,
rel: hintRel
};
if (hintAs) hint.as = hintAs;
addEarlyHintFetchAttrs(hint, attrs);
return hint;
}
function collectStaticHintsFromManifest(manifest, matchedRoutes) {
const hints = [];
for (const route of matchedRoutes) {
const routeManifest = manifest.routes[route.id];
if (!routeManifest) continue;
for (const link of routeManifest.preloads ?? []) {
const attrs = getScriptPreloadAttrs(manifest, link);
const hint = {
href: attrs.href,
rel: attrs.rel,
as: "script"
};
if (attrs.crossOrigin !== void 0) hint.crossOrigin = attrs.crossOrigin;
hints.push(hint);
}
for (const link of routeManifest.css ?? []) {
const stylesheetHref = getStylesheetHref(link);
if (manifest.inlineCss?.styles[stylesheetHref] !== void 0) continue;
const resolvedLink = resolveManifestCssLink(link);
const hint = {
href: stylesheetHref,
rel: "preload",
as: "style"
};
if (resolvedLink.crossOrigin !== void 0) hint.crossOrigin = resolvedLink.crossOrigin;
hints.push(hint);
}
}
return hints;
}
function collectDynamicHintsFromMatches(matches) {
const hints = [];
for (const match of matches) {
const links = match.links;
if (!Array.isArray(links)) continue;
for (const link of links) {
const hint = linkAttrsToEarlyHint(link);
if (hint) hints.push(hint);
}
}
return hints;
}
function createEarlyHintsEvent(opts) {
const nextHints = [];
const nextLinks = [];
for (const hint of opts.hints) {
const link = serializeEarlyHint(hint);
if (opts.sentLinks.has(link)) continue;
opts.sentLinks.add(link);
opts.sentHints.push(hint);
nextHints.push(hint);
nextLinks.push(link);
}
if (!nextHints.length && opts.phase !== "dynamic") return void 0;
return {
phase: opts.phase,
hints: nextHints,
links: nextLinks,
allHints: opts.sentHints.slice(),
allLinks: Array.from(opts.sentLinks)
};
}
function createResponseLinkHeaderEntries(opts) {
for (const hint of opts.hints) {
const link = serializeEarlyHint(hint);
if (opts.sentLinks.has(link)) continue;
opts.sentLinks.add(link);
opts.entries.push({
phase: opts.phase,
hint,
link
});
}
}
function getResponseLinkHeaderEntries(opts) {
if (!opts.filter) return opts.entries.map((entry) => entry.link);
try {
const links = [];
for (const entry of opts.entries) if (opts.filter(entry)) links.push(entry.link);
return links;
} catch (err) {
console.error("Error filtering response Link headers:", err);
return [];
}
}
function notifyEarlyHints(phase, event, onEarlyHints) {
try {
const result = onEarlyHints(event);
if (result) Promise.resolve(result).catch((err) => {
console.error(`Error sending ${phase} early hints:`, err);
});
} catch (err) {
console.error(`Error sending ${phase} early hints:`, err);
}
}
function getResponseLinkHeaderFilter(responseLinkHeader) {
if (typeof responseLinkHeader !== "object") return;
return responseLinkHeader.filter;
}
function appendResponseLinkHeaders(opts) {
for (const link of getResponseLinkHeaderEntries(opts)) opts.responseHeaders.append("Link", link);
}
function collectResponseLinkHeaderEntries(opts) {
for (let index = 0; index < opts.event.hints.length; index++) opts.entries.push({
phase: opts.phase,
hint: opts.event.hints[index],
link: opts.event.links[index]
});
}
function collectEarlyHintsPhase(opts) {
const event = opts.onEarlyHints ? createEarlyHintsEvent({
phase: opts.phase,
hints: opts.hints,
sentLinks: opts.sentLinks,
sentHints: opts.sentHints
}) : void 0;
if (event) notifyEarlyHints(opts.phase, event, opts.onEarlyHints);
if (!opts.responseLinkHeaderEntries) return;
if (event) {
collectResponseLinkHeaderEntries({
phase: opts.phase,
event,
entries: opts.responseLinkHeaderEntries
});
return;
}
createResponseLinkHeaderEntries({
phase: opts.phase,
hints: opts.hints,
sentLinks: opts.sentLinks,
entries: opts.responseLinkHeaderEntries
});
}
function createEarlyHintsCollector(opts) {
if (process.env.TSS_DEV_SERVER === "true" || !opts?.onEarlyHints && !opts?.responseLinkHeader) return;
const sentLinks = /* @__PURE__ */ new Set();
const sentHints = opts.onEarlyHints ? new Array() : void 0;
const responseLinkHeaderEntries = opts.responseLinkHeader ? new Array() : void 0;
const responseLinkHeaderFilter = getResponseLinkHeaderFilter(opts.responseLinkHeader);
return {
collectStatic: ({ manifest, matchedRoutes }) => {
if (!matchedRoutes?.length) return;
collectEarlyHintsPhase({
phase: "static",
hints: collectStaticHintsFromManifest(manifest, matchedRoutes),
sentLinks,
sentHints,
onEarlyHints: opts.onEarlyHints,
responseLinkHeaderEntries
});
},
collectDynamic: (matches) => {
collectEarlyHintsPhase({
phase: "dynamic",
hints: collectDynamicHintsFromMatches(matches),
sentLinks,
sentHints,
onEarlyHints: opts.onEarlyHints,
responseLinkHeaderEntries
});
},
appendResponseHeaders: (headers) => {
if (!responseLinkHeaderEntries?.length) return;
appendResponseLinkHeaders({
responseHeaders: headers,
entries: responseLinkHeaderEntries,
filter: responseLinkHeaderFilter
});
}
};
}
//#endregion
export { createEarlyHintsCollector };
//# sourceMappingURL=early-hints.js.map