UNPKG

@tanstack/start-server-core

Version:

Modern and scalable routing for React applications

254 lines (253 loc) 8.09 kB
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