// server/plugins/dedupe-inlined-css.ts // // Nuxt инлайнит CSS компонентов в , но его дедуп вычищает из // манифеста только для чанков, у которых есть `src`. CSS компонентов, попавших // на слитые (shared) чанки без `src` — например из-за experimentalMinChunkSize — // остаётся render-blocking , дублируя уже заинлайненные стили. // // Хук решает судьбу каждого такого (см. decideCssLinkAction): // заинлайнен целиком — линк убирается; заинлайнен частично — недостающие блоки // доинлайниваются, а линк убирается (иначе браузер тянет файл и дублирует уже // заинлайненные правила); не заинлайнен вовсе или хвост крупный — линк остаётся, // но неблокирующим. Никакого FOUC. // // Чистая логика разбора/решения вынесена в server/utils/css-dedupe.ts (покрыта тестами), // здесь — только IO (чтение .css через localFetch) и обход . import { BUILD_ASSETS_DIR, STYLESHEET_LINK_RE, HREF_RE, collectInlinedCss, decideCssLinkAction, makeStylesheetAsync, } from '../utils/css-dedupe'; // href -> содержимое .css-файла (null = прочитать не удалось) const cssTextCache = new Map(); /** Читает .css по href через localFetch и кэширует его содержимое (null при ошибке). */ const loadCssText = async (nitroApp: any, href: string): Promise => { if (cssTextCache.has(href)) return cssTextCache.get(href) ?? null; let text: string | null = null; try { const res = await nitroApp.localFetch(href, {}); if (res?.ok) text = (await res.text()) || null; } catch { text = null; } cssTextCache.set(href, text); return text; }; export default defineNitroPlugin((nitroApp) => { if (process.env.DISABLE_CSS_DEDUPE === '1') return; nitroApp.hooks.hook('render:html', async (html: { head: string[] }) => { const head = html.head; if (!head?.length) return; const inlinedNormalized = collectInlinedCss(head); if (!inlinedNormalized) return; for (let i = 0; i < head.length; i++) { const part = head[i]; if (!part.includes('rel="stylesheet"')) continue; const tags = part.match(STYLESHEET_LINK_RE); if (!tags) continue; let next = part; for (const tag of tags) { const href = tag.match(HREF_RE)?.[1]; if (!href || !href.startsWith(BUILD_ASSETS_DIR)) continue; const css = await loadCssText(nitroApp, href); if (!css) continue; const action = decideCssLinkAction(css, inlinedNormalized); if (action.kind === 'drop') next = next.replace(tag, ''); else if (action.kind === 'inline-tail') next = next.replace(tag, ``); else next = next.replace(tag, makeStylesheetAsync(tag)); } head[i] = next; } }); });