src/collectors/assets.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
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<string>();
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<string>();
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 };
}
|