UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

244 lines 8.78 kB
import * as Solid from 'solid-js'; import { _getAssetMatches, appendUniqueUserTags, escapeHtml, getAssetCrossOrigin, getScriptPreloadAttrs, resolveManifestCssLink, } from '@tanstack/router-core'; import { isServer } from '@tanstack/router-core/isServer'; import { useRouter } from './useRouter'; /** * Build the head/link/meta/script tags from the renderable presented prefix. * Used internally by `HeadContent`. */ export const useTags = (assetCrossOrigin) => { const router = useRouter(); const nonce = router.options.ssr?.nonce; const activeMatches = Solid.createMemo(() => _getAssetMatches(router.stores.matches.get())); const routeMeta = Solid.createMemo(() => activeMatches() .map((match) => match.meta) .filter((meta) => meta !== undefined)); const meta = Solid.createMemo(() => { const resultMeta = []; const metaByAttribute = {}; let title; const routeMetasArray = routeMeta(); for (let i = routeMetasArray.length - 1; i >= 0; i--) { const metas = routeMetasArray[i]; for (let j = metas.length - 1; j >= 0; j--) { const m = metas[j]; if (!m) { continue; } if (m.title) { if (!title) { title = { tag: 'title', children: m.title, }; } } else if ('script:ld+json' in m) { try { const json = JSON.stringify(m['script:ld+json']); resultMeta.push({ tag: 'script', attrs: { type: 'application/ld+json', }, children: escapeHtml(json), }); } catch { // Skip invalid JSON-LD objects. } } else { const attribute = m.name ?? m.property; if (attribute) { if (metaByAttribute[attribute]) { continue; } metaByAttribute[attribute] = true; } resultMeta.push({ tag: 'meta', attrs: { ...m, nonce, }, }); } } } if (title) { resultMeta.push(title); } if (nonce) { resultMeta.push({ tag: 'meta', attrs: { property: 'csp-nonce', content: nonce, }, }); } resultMeta.reverse(); return resultMeta; }); const links = Solid.createMemo(() => { return activeMatches() .flatMap((match) => match.links ?? []) .filter((link) => link !== undefined) .map((link) => ({ tag: 'link', attrs: { ...link, nonce, }, })); }); const retainedManifestCssTags = new Map(); const manifestCssTags = Solid.createMemo(() => { const manifest = router.ssr?.manifest; const tags = []; if (!manifest) { return tags; } for (const match of activeMatches()) { for (const link of manifest.routes[match.routeId]?.css ?? []) { const resolvedLink = resolveManifestCssLink(link); const tag = { tag: 'link', attrs: { rel: 'stylesheet', ...resolvedLink, crossOrigin: getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ?? resolvedLink.crossOrigin, nonce, }, }; const key = JSON.stringify(tag); if (!retainedManifestCssTags.has(key)) { retainedManifestCssTags.set(key, tag); } } } // Lazy modules are cached and do not reinsert their CSS when revisited. tags.push(...retainedManifestCssTags.values()); if (manifest.inlineStyle) { tags.push({ tag: 'style', attrs: { ...manifest.inlineStyle.attrs, nonce, }, children: manifest.inlineStyle.children, inlineCss: true, }); } return tags; }); const preloadLinks = Solid.createMemo(() => { const manifest = router.ssr?.manifest; const tags = []; for (const match of activeMatches()) { for (const preload of manifest?.routes[match.routeId]?.preloads ?? []) { if (!preload) { continue; } tags.push({ tag: 'link', attrs: { ...getScriptPreloadAttrs(manifest, preload, assetCrossOrigin), nonce, }, }); } } return tags; }); const styles = Solid.createMemo(() => { return activeMatches() .flatMap((match) => match.styles ?? []) .filter((style) => style !== undefined) .map(({ children, ...attrs }) => ({ tag: 'style', attrs: { ...attrs, nonce, }, children: children, })); }); const headScripts = Solid.createMemo(() => { return activeMatches() .flatMap((match) => match.headScripts ?? []) .filter((script) => script !== undefined) .map(({ children, ...attrs }) => ({ tag: 'script', attrs: { ...attrs, nonce, }, children: children, })); }); return Solid.createMemo(() => { const next = []; appendUniqueUserTags(next, meta()); appendUniqueUserTags(next, links()); next.push(...manifestCssTags()); next.push(...preloadLinks()); appendUniqueUserTags(next, styles()); appendUniqueUserTags(next, headScripts()); return next; }); }; const INLINE_CSS_HYDRATION_ATTR = 'data-tsr-inline-css'; /** * Convert the router-managed tags into head registry descriptors for * `useHead`. Pure data mapping — it is evaluated inside the registry's own * flush boundary (server) / effect (client), so it must not allocate * reactive owners. */ export function toHeadTags(tags) { return tags.map(toHeadTag); } function toHeadTag(t) { const props = { ...t.attrs }; let children = t.children; if (t.tag === 'style' && t.inlineCss && (process.env.TSS_INLINE_CSS_ENABLED === 'true' || (process.env.TSS_INLINE_CSS_ENABLED === undefined && isServer))) { // Mark the inline-CSS style so the client can find it again: the // serialized manifest omits the CSS text, so on the client the tag // arrives with `children === undefined` and the text is recovered from // the server-rendered element. props[INLINE_CSS_HYDRATION_ATTR] = ''; } if (t.tag === 'style' && t.inlineCss && children === undefined && typeof document !== 'undefined') { children = document.querySelector(`style[${INLINE_CSS_HYDRATION_ATTR}]`)?.textContent ?? ''; } if (children !== undefined) { props.children = children; } const headTag = { tag: t.tag, props }; // Inline scripts and styles have no natural registry identity: the // registry assigns them per-runtime unique ids, which can never match // between server and client, so hydration would append a client copy // next to the server-rendered one. A stable content-derived key gives // both runtimes the same identity to reconcile on. if (t.tag === 'style' || (t.tag === 'script' && props.src === undefined)) { headTag.key = `tsr-${hashString(t.tag + JSON.stringify(props))}`; } return headTag; } function hashString(s) { let h = 5381; for (let i = 0; i < s.length; i++) { h = ((h << 5) + h + s.charCodeAt(i)) | 0; } return (h >>> 0).toString(36); } //# sourceMappingURL=headContentUtils.jsx.map