all repos — red-anatomia @ 43cc40161c75c7c9bb5bef2c1ce2d551b7f24484

red-anatomia

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