Dosson 0.9.10: source code
This is every file of Dosson 0.9.10 that is not minified third-party code, one after the other. The extension is
plain JavaScript with no build step, so this is what runs in your browser.
Files:
manifest.json
index.html
index.js
background.js
mainThread.js
frames.js
perf.js
disableClear.js
security/secrets.js
detect/detect.js
detect/detectors.json
detect/js/ace.js
detect/js/aframe.js
detect/js/ag-grid.js
detect/js/alpine.js
detect/js/amcharts.js
detect/js/angular.js
detect/js/angularjs.js
detect/js/animejs.js
detect/js/aos.js
detect/js/apexcharts.js
detect/js/astro.js
detect/js/aura.js
detect/js/aurelia.js
detect/js/axios.js
detect/js/babylon.js
detect/js/backbone.js
detect/js/barba.js
detect/js/blazor.js
detect/js/canjs.js
detect/js/capacitor.js
detect/js/chartjs.js
detect/js/choices.js
detect/js/ckeditor.js
detect/js/cleave.js
detect/js/clipboardjs.js
detect/js/clojurescript.js
detect/js/closure.js
detect/js/codemirror.js
detect/js/cordova.js
detect/js/core-js.js
detect/js/cyclejs.js
detect/js/d3.js
detect/js/datatables.js
detect/js/dayjs.js
detect/js/devextreme.js
detect/js/docusaurus.js
detect/js/dojo.js
detect/js/dompurify.js
detect/js/draftjs.js
detect/js/dropzone.js
detect/js/echarts.js
detect/js/elm.js
detect/js/ember.js
detect/js/extjs.js
detect/js/fabric.js
detect/js/fancybox.js
detect/js/filepond.js
detect/js/flatpickr.js
detect/js/flickity.js
detect/js/floating-ui.js
detect/js/flutter.js
detect/js/framer-motion.js
detect/js/framework7.js
detect/js/fresh.js
detect/js/fusejs.js
detect/js/gatsby.js
detect/js/glide.js
detect/js/google-charts.js
detect/js/google-maps.js
detect/js/gridsome.js
detect/js/gsap.js
detect/js/gwt.js
detect/js/hammerjs.js
detect/js/handlebars.js
detect/js/handsontable.js
detect/js/highcharts.js
detect/js/highlightjs.js
detect/js/hlsjs.js
detect/js/howler.js
detect/js/htmx.js
detect/js/hyperapp.js
detect/js/immutable.js
detect/js/inertia.js
detect/js/inferno.js
detect/js/inputmask.js
detect/js/instantsearch.js
detect/js/interactjs.js
detect/js/intercooler.js
detect/js/intl-tel-input.js
detect/js/ionic.js
detect/js/isotope.js
detect/js/jquery.js
detect/js/jquery-migrate.js
detect/js/jquery-mobile.js
detect/js/jquery-ui.js
detect/js/jquery-validation.js
detect/js/js-cookie.js
detect/js/katex.js
detect/js/kendo.js
detect/js/knockout.js
detect/js/konva.js
detect/js/lazysizes.js
detect/js/leaflet.js
detect/js/lenis.js
detect/js/lexical.js
detect/js/lightbox2.js
detect/js/lit.js
detect/js/livewire.js
detect/js/locomotive-scroll.js
detect/js/lodash.js
detect/js/lottie.js
detect/js/lwc.js
detect/js/mapbox-gl.js
detect/js/maplibre.js
detect/js/marked.js
detect/js/marko.js
detect/js/masonry.js
detect/js/mathjax.js
detect/js/mermaid.js
detect/js/meteor.js
detect/js/million.js
detect/js/mithril.js
detect/js/mobx.js
detect/js/modernizr.js
detect/js/moment.js
detect/js/monaco.js
detect/js/mootools.js
detect/js/next.js
detect/js/nuxt.js
detect/js/onsen.js
detect/js/openlayers.js
detect/js/owl-carousel.js
detect/js/p5.js
detect/js/paperjs.js
detect/js/parsley.js
detect/js/petite-vue.js
detect/js/phaser.js
detect/js/phoenix-liveview.js
detect/js/photoswipe.js
detect/js/pikaday.js
detect/js/pinia.js
detect/js/pixi.js
detect/js/plotly.js
detect/js/plyr.js
detect/js/polymer.js
detect/js/popper.js
detect/js/preact.js
detect/js/prism.js
detect/js/prosemirror.js
detect/js/prototype.js
detect/js/quill.js
detect/js/qwik.js
detect/js/ractive.js
detect/js/react.js
detect/js/react-router.js
detect/js/recharts.js
detect/js/redux.js
detect/js/redwood.js
detect/js/requirejs.js
detect/js/riot.js
detect/js/rxjs.js
detect/js/scriptaculous.js
detect/js/scrollreveal.js
detect/js/select2.js
detect/js/sencha-touch.js
detect/js/slate.js
detect/js/slick.js
detect/js/socketio.js
detect/js/solid.js
detect/js/sortablejs.js
detect/js/splide.js
detect/js/stencil.js
detect/js/stimulus.js
detect/js/svelte.js
detect/js/sveltekit.js
detect/js/swiper.js
detect/js/syncfusion.js
detect/js/three.js
detect/js/tinymce.js
detect/js/tippy.js
detect/js/tiptap.js
detect/js/tom-select.js
detect/js/turbo.js
detect/js/turbolinks.js
detect/js/underscore.js
detect/js/unpoly.js
detect/js/uppy.js
detect/js/vaadin.js
detect/js/velocity.js
detect/js/videojs.js
detect/js/vite.js
detect/js/vitepress.js
detect/js/vue.js
detect/js/vue-router.js
detect/js/vuepress.js
detect/js/vuex.js
detect/js/webix.js
detect/js/webpack.js
detect/js/wicket.js
detect/js/yui.js
detect/js/zepto.js
detect/css/7css.js
detect/css/98css.js
detect/css/adminlte.js
detect/css/angular-material.js
detect/css/animate-css.js
detect/css/antd.js
detect/css/aphrodite.js
detect/css/arco.js
detect/css/atlaskit.js
detect/css/atomizer.js
detect/css/balloon-css.js
detect/css/baseweb.js
detect/css/basscss.js
detect/css/blueprint.js
detect/css/bootstrap.js
detect/css/bootstrap-icons.js
detect/css/bootstrap-reboot.js
detect/css/bootstrap-vue.js
detect/css/bootswatch.js
detect/css/bourbon.js
detect/css/boxicons.js
detect/css/buefy.js
detect/css/bulma.js
detect/css/carbon.js
detect/css/chakra.js
detect/css/chota.js
detect/css/cirrus.js
detect/css/clarity.js
detect/css/coreui.js
detect/css/css-modules.js
detect/css/daisyui.js
detect/css/devextreme-themes.js
detect/css/divi.js
detect/css/element.js
detect/css/elementor.js
detect/css/emotion.js
detect/css/eui.js
detect/css/evergreen.js
detect/css/feather.js
detect/css/flowbite.js
detect/css/fluent-ui.js
detect/css/font-awesome.js
detect/css/foundation.js
detect/css/framework7-css.js
detect/css/geist.js
detect/css/glyphicons.js
detect/css/goober.js
detect/css/google-fonts.js
detect/css/grommet.js
detect/css/gutenberg.js
detect/css/halfmoon.js
detect/css/headlessui.js
detect/css/heroicons.js
detect/css/heroui.js
detect/css/hint-css.js
detect/css/hover-css.js
detect/css/ionic-css.js
detect/css/ionicons.js
detect/css/jss.js
detect/css/kendo-themes.js
detect/css/kube.js
detect/css/linaria.js
detect/css/line-awesome.js
detect/css/lucide.js
detect/css/magic-css.js
detect/css/mantine.js
detect/css/material-icons.js
detect/css/material-tailwind.js
detect/css/material-web.js
detect/css/materialize.js
detect/css/mdbootstrap.js
detect/css/mdc.js
detect/css/mdi.js
detect/css/mdl.js
detect/css/milligram.js
detect/css/mini-css.js
detect/css/modern-normalize.js
detect/css/mui.js
detect/css/mvp-css.js
detect/css/naive-ui.js
detect/css/neat.js
detect/css/nes-css.js
detect/css/new-css.js
detect/css/normalize.js
detect/css/octicons.js
detect/css/onsen-css.js
detect/css/open-props.js
detect/css/oruga.js
detect/css/panda.js
detect/css/papercss.js
detect/css/patternfly.js
detect/css/phosphor.js
detect/css/picnic.js
detect/css/pico.js
detect/css/polaris.js
detect/css/preline.js
detect/css/prime.js
detect/css/primer.js
detect/css/pure.js
detect/css/quasar.js
detect/css/radix.js
detect/css/react-bootstrap.js
detect/css/reactstrap.js
detect/css/rebass.js
detect/css/remixicon.js
detect/css/sakura.js
detect/css/sanitize-css.js
detect/css/semantic-ui.js
detect/css/shadcn.js
detect/css/shoelace.js
detect/css/shopify-dawn.js
detect/css/simple-css.js
detect/css/simple-line-icons.js
detect/css/skeleton.js
detect/css/slds.js
detect/css/spectre.js
detect/css/spectrum.js
detect/css/stitches.js
detect/css/styled-components.js
detect/css/stylex.js
detect/css/susy.js
detect/css/syncfusion-themes.js
detect/css/tabler.js
detect/css/tabler-icons.js
detect/css/tachyons.js
detect/css/tailwind.js
detect/css/tdesign.js
detect/css/theme-ui.js
detect/css/themify.js
detect/css/tuicss.js
detect/css/twind.js
detect/css/uikit.js
detect/css/unocss.js
detect/css/vanilla-extract.js
detect/css/vant.js
detect/css/view-ui.js
detect/css/vuetify.js
detect/css/water-css.js
detect/css/windicss.js
detect/css/wpbakery.js
detect/css/xp-css.js
Third-party code, minified and not repeated here:
3rd-party/custom-elements.min.js: custom-elements polyfill 1.6.0 (@webcomponents/custom-elements, BSD-3-Clause)
3rd-party/tuicss.min.js: TuiCSS (MIT)
web-vitals 3.5.2 (Apache-2.0) and TuiCSS's stylesheet are pasted inside index.js.
==============================================================================
manifest.json (68 lines)
==============================================================================
{
"name": "Dosson",
"version": "0.9.10",
"manifest_version": 3,
"description": "Performance and usage, DOS style",
"homepage_url": "https://bocares.com/dosson/",
"icons": {
"16": "images/16x16.png",
"48": "images/48x48.png",
"128": "images/128x128.png"
},
"action": {
"default_icon": {
"16": "images/16x16.png",
"48": "images/48x48.png",
"128": "images/128x128.png"
},
"default_title": "Dosson"
},
"host_permissions": [
"*://*/*"
],
"permissions": [
"tabs",
"storage",
"activeTab",
"scripting"
],
"web_accessible_resources": [
{
"resources": [
"mainThread.js",
"disableClear.js"
],
"matches": [
"*://*/*"
]
}
],
"content_scripts": [
{
"matches": [
"*://*/*"
],
"js": [
"perf.js"
],
"run_at": "document_start"
},
{
"matches": [
"*://*/*"
],
"css": [
],
"js": [
"3rd-party/custom-elements.min.js",
"3rd-party/tuicss.min.js",
"frames.js",
"index.js"
],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js"
}
}
==============================================================================
index.html (18 lines)
==============================================================================
Welcome to dosson
==============================================================================
index.js (3646 lines)
==============================================================================
try {
let script = document.createElement('script');
script.src = chromeRuntimeGetURL('disableClear.js');
(document.head || document.documentElement).appendChild(script);
} catch (e) {
__error(`Caught error trying to add a script: ${e}`)
}
function __info(message) {
console.log(`%c--> ${message}`, 'background: green; color: white');
}
function __error(message) {
console.error(`%c--> ${message}`, 'background: red; color: white');
}
// a key Dosson used: no default action, and the page never sees it. Dosson listens on the window in the capture
// phase, so this runs before the page's own listeners (Gmail's ← → would move between emails)
function consumeKey(event) {
event.preventDefault();
event.stopPropagation();
}
/* chrome function. Let's make alternative for our test page */
function chromeStorageSyncSet(toPersist, callback) {
if (chrome && chrome.storage && chrome.storage.sync && chrome.storage.sync.set) {
chrome.storage.sync.set(toPersist, callback);
}
else {
__info(`Going to be using localstorage instead of chrome.storage.sync.set`);
for (let key in toPersist) {
localStorage.setItem(key, JSON.stringify(toPersist[key]));
}
callback();
}
}
function chromeStorageSyncGet(toRetrieveArray, callback) {
if (chrome && chrome.storage && chrome.storage.sync && chrome.storage.sync.get) {
chrome.storage.sync.get(toRetrieveArray, callback);
} else {
__info(`Going to be using localstorage instead of chrome.storage.sync.get`);
const result = {};
toRetrieveArray.forEach(key => {
const value = localStorage.getItem(key);
if (value !== null) {
result[key] = JSON.parse(value);
}
})
callback(result);
}
};
// the test page is a single tab, so it has nothing to listen to
function chromeStorageOnChanged(callback) {
if (chrome && chrome.storage && chrome.storage.onChanged) {
chrome.storage.onChanged.addListener(callback);
}
}
// whether Dosson shows in this tab. background.js keeps it per tab id; the test page uses sessionStorage, which is
// per tab too. A page left from before the extension reloaded gets no answer, so it stays hidden
async function getTabVisible() {
if (!isExtension()) {
return sessionStorage.getItem('dossonVisible') === 'true';
}
return (await sendToExtension({ type: 'dossonGetVisible' })).visible === true;
}
function setTabVisible(visible) {
if (!isExtension()) {
sessionStorage.setItem('dossonVisible', visible);
return;
}
sendToExtension({ type: 'dossonSetVisible', visible }).then(response => {
if (response.error) {
__error(`Could not save Dosson's visibility: ${response.error}`);
}
});
}
function chromeRuntimeGetURL(script) {
if (chrome && chrome.runtime && chrome.runtime.getURL) {
return chrome.runtime.getURL('mainThread.js');
} else {
return "./" + script
}
}
// Dosson shows or hides when dossonToggleKey is tapped dossonTogglePresses times in a row,
// each tap less than DOSSON_TOGGLE_DELAY ms after the previous one
const DOSSON_TOGGLE_KEYS = [
{ key: 'Escape', label: 'Esc' },
{ key: 'Control', label: 'Ctrl' },
{ key: 'Shift', label: 'Shift' },
{ key: 'Alt', label: 'Alt' }
];
const DOSSON_TOGGLE_PRESSES = [2, 3, 4, 5];
const DOSSON_TOGGLE_DELAY = 500;
// defaults until the storage answers; the property names are the storage keys
const dossonSettings = { dossonToggleKey: 'Escape', dossonTogglePresses: 2, dossonFontSize: 21 };
// Dosson's font size in px, whatever the page's (TuiCSS makes everything inherit it), within these bounds
const DOSSON_FONT_SIZE_MIN = 10;
const DOSSON_FONT_SIZE_MAX = 40;
// the font size the px sizes of levels (widths, offsets) were made for
const DOSSON_FONT_SIZE_DESIGN = 17;
// keep only the stored values Dosson knows about
function applyDossonSettings(stored) {
if (DOSSON_TOGGLE_KEYS.some(toggleKey => toggleKey.key === stored.dossonToggleKey)) {
dossonSettings.dossonToggleKey = stored.dossonToggleKey;
}
if (DOSSON_TOGGLE_PRESSES.includes(stored.dossonTogglePresses)) {
dossonSettings.dossonTogglePresses = stored.dossonTogglePresses;
}
if (Number.isInteger(stored.dossonFontSize) && stored.dossonFontSize >= DOSSON_FONT_SIZE_MIN && stored.dossonFontSize <= DOSSON_FONT_SIZE_MAX) {
dossonSettings.dossonFontSize = stored.dossonFontSize;
}
applyDossonFontSize();
}
// on : its levels and panels live in its shadow root, so they all inherit it
function applyDossonFontSize() {
const levelOne = document.querySelector('level-one');
if (levelOne) {
levelOne.style.fontSize = `${dossonSettings.dossonFontSize}px`;
if (levelOne.isRendered) {
levelOne.style.width = dossonPx(200);
levelOne.layout();
}
}
}
// a size in px made for a 17px font (DOSSON_FONT_SIZE_DESIGN), scaled to the one Settings chose: levels keep their
// proportions. In px, not em: keepInViewport does arithmetic on them
function dossonPx(px) {
return `${Math.round(px * dossonSettings.dossonFontSize / DOSSON_FONT_SIZE_DESIGN)}px`;
}
// the size Settings → Bigger (step 1) or Smaller text (step -1) switches to
function dossonFontSizeAfter(step) {
return Math.min(DOSSON_FONT_SIZE_MAX, Math.max(DOSSON_FONT_SIZE_MIN, dossonSettings.dossonFontSize + step));
}
// the font size Dosson switches to after that step, like "22px", or "max"/"min" when it can't change
function dossonFontSizeLabel(step) {
const size = dossonFontSizeAfter(step);
return size === dossonSettings.dossonFontSize ? (step > 0 ? 'max' : 'min') : `${size}px`;
}
function saveDossonSetting(name, value) {
dossonSettings[name] = value;
chromeStorageSyncSet({ [name]: value }, () => { __info(`Dosson setting ${name} set to ${value}`); });
}
// what each kind of iframe (see getIframeKind in frames.js) is. The page around an iframe can only read the
// window of a same-origin one; Dosson reads the others through background.js
const IFRAME_KINDS = {
same: 'Same origin as the page around it',
cross: "Other origin: the page around it can't read its window",
sandbox: 'Sandboxed without allow-same-origin: a unique origin, like cross',
srcdoc: 'HTML inlined in the srcdoc attribute, same origin as the page around it',
blank: 'No src (about:blank), usually filled by a script, same origin as the page around it',
data: 'data: URL: a unique origin, like cross',
blob: 'blob: URL created by a script, same origin as that script',
ext: 'Added by another extension: Chrome keeps Dosson out'
};
// wide enough for a 20-character host and "sandbox >"
const IFRAME_LIST_WIDTH = '330px';
// wide enough for "Latest released" and a GitHub owner/repo
const FRAMEWORK_DETAILS_WIDTH = '400px';
// wide enough for "Supabase service-role key" and "MEDIUM >"
const SECRET_LIST_WIDTH = '440px';
const SECRET_DETAILS_WIDTH = '520px';
// a level shows 12 rows. A list longer than VIRTUAL_LIST_MIN only has those 12 in the DOM (see Level2.renderRows)
const LEVEL_ROWS = 12;
const VIRTUAL_LIST_MIN = 100;
// the object a finding's path is in: a.b for a.b.c, a.b for a.b["x-y"] or a.b[0]; '' when there's none (a cookie)
function secretParent(display) {
const parent = display.replace(/(?:\.[A-Za-z_$][\w$]*|\[(?:\d+|"(?:[^"\\]|\\.)*")\])$/, '');
return parent !== display ? parent : '';
}
const SECRET_WHERE = { window: 'Variable', localStorage: 'localStorage', sessionStorage: 'sessionStorage', cookie: 'Cookie' };
// Settings → Privacy
const DOSSON_PRIVACY = [
"We don't collect anything: no analytics, nothing from the websites you visit. There is no Dosson server that it connects to.",
"Everything Dosson shows is read and computed in your browser, on the page you're looking at. Your settings are saved by Chrome, like any extension's.",
"Opening a framework's details asks deps.dev or GitHub when its versions came out. The request names the framework, nothing about the page.",
"The full policy: bocares.com/dosson/privacy"
];
// wide enough for "API calls (1,234)" and "12.3 MB >"
const NETWORK_LIST_WIDTH = '400px';
// wide enough for a 25-character file name and "1200×800 → 300×200"
const RESOURCE_LIST_WIDTH = '520px';
// Performance → Network, in the order shown
const RESOURCE_KINDS = [
{ id: 'document', name: 'Documents', help: 'The page itself and its iframes (their own requests are not counted)' },
{ id: 'js', name: 'JavaScript' },
{ id: 'css', name: 'CSS' },
{ id: 'api', name: 'API calls', help: 'fetch, XMLHttpRequest and sendBeacon' },
{ id: 'font', name: 'Fonts' },
{ id: 'image', name: 'Images' },
{ id: 'media', name: 'Media', help: 'Video, audio and subtitles' },
{ id: 'other', name: 'Other' }
];
const RESOURCE_EXTENSIONS = {
js: ['js', 'mjs', 'cjs'],
css: ['css'],
font: ['woff', 'woff2', 'ttf', 'otf', 'eot'],
image: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'ico', 'bmp'],
media: ['mp4', 'webm', 'mp3', 'ogg', 'oga', 'm4a', 'wav', 'm3u8', 'mpd', 'vtt'],
api: ['json'],
document: ['html', 'htm']
};
function formatBytes(bytes) {
if (bytes < 1000) {
return `${bytes} B`;
}
const units = ['kB', 'MB', 'GB'];
let value = bytes / 1000;
let unit = 0;
while (value >= 1000 && unit < units.length - 1) {
value /= 1000;
unit++;
}
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
}
// the page's requests: every one perf.js saw from document_start, or (on test.html) the 250 Chrome keeps
function getResourceEntries() {
const navigation = performance.getEntriesByType('navigation');
const watched = globalThis[Symbol.for('dosson')]?.resources;
// extensions' files (Dosson's mainThread.js…) are not the page's
const ownFiles = entry => !entry.name.startsWith('chrome-extension:');
if (watched) {
return { entries: [...navigation, ...watched.entries].filter(ownFiles), dropped: watched.dropped };
}
try {
// only raises the limit for what comes next: what Chrome already dropped is gone
performance.setResourceTimingBufferSize(Math.max(performance.getEntriesByType('resource').length, 10000));
} catch (e) {
// older browsers
}
return { entries: [...navigation, ...performance.getEntriesByType('resource')].filter(ownFiles), dropped: 0 };
}
function getResourceKind(entry) {
const initiator = entry.initiatorType;
if (['fetch', 'xmlhttprequest', 'beacon'].includes(initiator)) {
return 'api';
}
if (['navigation', 'iframe', 'frame'].includes(initiator)) {
return 'document';
}
// the response's MIME type (Chrome 129+), then the file extension, then what asked for it
const type = entry.contentType || '';
if (type) {
if (type.includes('css')) return 'css';
if (type.includes('javascript') || type.includes('ecmascript')) return 'js';
if (type.startsWith('font/') || type.includes('font')) return 'font';
if (type.startsWith('image/')) return 'image';
if (type.startsWith('video/') || type.startsWith('audio/') || type.includes('mpegurl') || type.includes('dash')) return 'media';
if (type.includes('json')) return 'api';
if (type.includes('html')) return 'document';
}
let extension = '';
try {
extension = new URL(entry.name).pathname.match(/\.([a-z0-9]+)$/i)?.[1].toLowerCase() || '';
} catch (e) {
// not a URL
}
const byExtension = Object.keys(RESOURCE_EXTENSIONS).find(kind => RESOURCE_EXTENSIONS[kind].includes(extension));
if (byExtension) {
return byExtension;
}
return { script: 'js', img: 'image', image: 'image', video: 'media', audio: 'media', track: 'media' }[initiator] || 'other';
}
// a URL's last path segment, or its host
function shortUrlName(url) {
try {
const { hostname, pathname } = new URL(url);
const last = pathname.split('/').filter(Boolean).pop();
return last ? decodeURIComponent(last) : hostname;
} catch (e) {
return url;
}
}
// what the network gave each request: bytes, or why it has none
function getResourceSize(entry) {
if (entry.transferSize > 0) {
return { transfer: entry.transferSize, decoded: entry.decodedBodySize || 0, state: 'network' };
}
if (entry.decodedBodySize > 0) {
return { transfer: 0, decoded: entry.decodedBodySize, state: 'cache' };
}
// another origin without Timing-Allow-Origin hides its size (or the request failed or was empty)
return { transfer: 0, decoded: 0, state: 'hidden' };
}
// a short CSS-like name for an element: tag#id.class
function describeElement(element) {
const id = element.id ? `#${element.id}` : '';
const className = typeof element.className === 'string' ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).map(name => `.${name}`).join('') : '';
return `${element.localName}${id}${className}`;
}
// walk the page's elements, open shadow roots included, Dosson's own left out.
// A loop, not recursion: some pages nest deeper than the call stack
function measureDom() {
const stats = {
elements: 0, maxDepth: 0, maxChildren: 0, maxChildrenElement: null, shadowRoots: [], customElements: 0,
iframes: 0, images: [], scripts: 0, inlineScript: 0, inlineStyle: 0
};
const stack = [[document.documentElement, 1]];
while (stack.length) {
const [element, depth] = stack.pop();
// Dosson's menu, templates and mainThread.js tags
if (element.localName === 'level-one' || element.id === 'dosson_action-template' || element.id === 'dosson_action-header'
|| (element.localName === 'script' && element.src.startsWith('chrome-extension:'))) {
continue;
}
stats.elements++;
stats.maxDepth = Math.max(stats.maxDepth, depth);
if (element.childElementCount > stats.maxChildren) {
stats.maxChildren = element.childElementCount;
stats.maxChildrenElement = element;
}
const name = element.localName;
if (name.includes('-')) {
stats.customElements++;
}
if (name === 'iframe' || name === 'frame') {
stats.iframes++;
} else if (name === 'img') {
stats.images.push(element);
} else if (name === 'script') {
stats.scripts++;
if (!element.src) {
stats.inlineScript += element.textContent.length;
}
} else if (name === 'style') {
stats.inlineStyle += element.textContent.length;
}
if (element.shadowRoot) {
stats.shadowRoots.push(element.shadowRoot);
[...element.shadowRoot.children].forEach(child => stack.push([child, depth + 1]));
}
[...element.children].forEach(child => stack.push([child, depth + 1]));
}
return stats;
}
// count the rules of every readable stylesheet. Another origin's sheet without CORS throws on cssRules,
// and so can a sheet still loading: those are counted as not readable
function measureCss(shadowRoots) {
const stats = { sheets: 0, unreadable: [], total: 0, style: 0, grouping: 0, fontFace: 0, keyframes: 0, import: 0, other: 0 };
const seen = new Set();
const readSheet = sheet => {
if (!sheet || seen.has(sheet)) {
return;
}
seen.add(sheet);
stats.sheets++;
let rules;
try {
rules = sheet.cssRules;
} catch (e) {
rules = null;
}
if (!rules) {
stats.unreadable.push(sheet.href || 'inline');
return;
}
countRules(rules);
};
const countRules = rules => {
for (const rule of rules) {
stats.total++;
if (rule instanceof CSSStyleRule) {
stats.style++;
} else if (rule instanceof CSSImportRule) {
stats.import++;
readSheet(rule.styleSheet);
continue;
} else if (rule instanceof CSSFontFaceRule) {
stats.fontFace++;
} else if (rule instanceof CSSKeyframesRule) {
// its keyframes are not rules of their own
stats.keyframes++;
continue;
} else if (rule instanceof CSSGroupingRule || rule instanceof CSSConditionRule) {
stats.grouping++;
} else {
stats.other++;
}
// @media, @supports, @layer, @container and nested style rules hold more rules
if (rule.cssRules?.length) {
countRules(rule.cssRules);
}
}
};
[document, ...shadowRoots].forEach(root => {
[...root.styleSheets].forEach(readSheet);
(root.adoptedStyleSheets || []).forEach(readSheet);
});
return stats;
}
// mainThread.js reads globals in the page's main world: the page's own when asked on window, a same-origin iframe's
// when asked on its