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