import * as cheerio from 'cheerio'; import type { Observations, AnatomiaFrontend } from '../core/types.js'; const EXT_IMAGEM = /\.(avif|webp|png|jpe?g|gif|svg|ico)(\?|$)/i; export function coletarAnatomia(obs: Observations): void { if (!obs.html) return; let $: cheerio.CheerioAPI; try { $ = cheerio.load(obs.html); } catch { return; } const scripts = $('script'); const scriptsComSrc = obs.scriptSrc; const assincrono = scripts.toArray().some((el) => { const a = $(el); return a.attr('async') != null || a.attr('defer') != null || a.attr('type') === 'module'; }); const provavelChunking = scriptsComSrc.filter((s) => /[.-][0-9a-f]{6,}\.js/i.test(s)).length >= 2; const linksCss = $('link[rel="stylesheet"], link[rel="preload"][as="style"]').length; const inlineCritico = $('style').toArray().some((el) => ($(el).text() || '').length > 200); const imagens = coletarImagens($); const lazyLoading = $('img[loading="lazy"], img[data-src], img[data-lazy]').length > 0; const fontes = coletarFontes($, obs); const anatomia: AnatomiaFrontend = { javascript: { arquivos: scriptsComSrc.length, assincrono, provavelChunking, }, css: { arquivos: linksCss, inlineCritico, }, imagens: { total: imagens.total, formatos: [...imagens.formatos], lazyLoading, }, fontes, }; obs.anatomia = anatomia; } function coletarImagens($: cheerio.CheerioAPI) { const formatos = new Set(); let total = 0; $('img').each((_, el) => { total += 1; const src = $(el).attr('src') ?? $(el).attr('data-src') ?? ''; const m = src.match(EXT_IMAGEM); if (m?.[1]) formatos.add(m[1].toLowerCase()); }); $('source[srcset], source[type]').each((_, el) => { const tipo = $(el).attr('type'); if (tipo?.startsWith('image/')) formatos.add(tipo.replace('image/', '').toLowerCase()); const srcset = $(el).attr('srcset') ?? ''; const m = srcset.match(EXT_IMAGEM); if (m?.[1]) formatos.add(m[1].toLowerCase()); }); return { total, formatos }; } function coletarFontes($: cheerio.CheerioAPI, obs: Observations) { const familias = new Set(); let externas = false; let locais = false; const estilos = $('style') .toArray() .map((el) => $(el).text() || '') .join('\n'); for (const m of estilos.matchAll(/font-family\s*:\s*([^;"}]+)/gi)) { const nome = m[1]?.split(',')[0]?.replace(/['"]/g, '').trim(); if (nome) familias.add(nome.toLowerCase()); } if (/@font-face[\s\S]*?url\(\s*['"]?\//i.test(estilos)) locais = true; for (const href of obs.linksAssets) { if (/fonts\.googleapis\.com|fonts\.gstatic\.com|use\.typekit|fonts\.adobe/i.test(href)) { externas = true; } if (/\.(woff2?|ttf|otf|eot)(\?|$)/i.test(href)) { try { const abs = new URL(href, obs.urlFinal || obs.urlSolicitada); const hostAlvo = new URL(obs.urlFinal || obs.urlSolicitada).hostname; if (abs.hostname === hostAlvo) locais = true; else externas = true; } catch { locais = true; } } } return { familias: familias.size, locais, externas }; }