release: WRNexusJS 0.2.75
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
export const DEV_TOOLBAR_RUNTIME = String.raw`(() => {
|
||||
if (window.__wrnexusDevToolbar) return;
|
||||
const KEY = "__wrnexus_dev_toolbar_settings__";
|
||||
const state = { open:false, issues:[], report:null, search:"", config:{ position:"bottom-center", scanOnNavigation:true, scanOnHmr:true } };
|
||||
try { state.config = Object.assign(state.config, JSON.parse(localStorage.getItem(KEY) || "{}")); } catch {}
|
||||
const host = document.createElement("wrnexus-dev-toolbar");
|
||||
host.setAttribute("data-wrnexus-dev-toolbar", "");
|
||||
document.body.appendChild(host);
|
||||
const root = host.attachShadow({mode:"open"});
|
||||
const style = document.createElement("style");
|
||||
fetch("/__wrnexus/dev-toolbar.css").then(r => r.ok ? r.text() : "").then(css => style.textContent = css).catch(() => {});
|
||||
root.appendChild(style);
|
||||
const shell = document.createElement("div"); shell.className="wrn-shell"; shell.dataset.position=state.config.position;
|
||||
shell.innerHTML='<section class="wrn-panel"><header class="wrn-panel-head"><div><div class="wrn-panel-title">WRNexus DevToolbar</div><div class="wrn-panel-meta"></div></div><button class="wrn-small" data-close>Close</button></header><div class="wrn-toolbar-row"><input class="wrn-search" placeholder="Search issues, rules, files…"><button class="wrn-small" data-scan>Rescan</button><button class="wrn-small" data-clear>Clear</button></div><div class="wrn-list"></div></section><nav class="wrn-bar"><span class="wrn-brand">WRNexus</span><button class="wrn-count" data-filter="error"><span class="wrn-dot error"></span><span data-errors>0</span></button><button class="wrn-count" data-filter="warning"><span class="wrn-dot warning"></span><span data-warnings>0</span></button><button class="wrn-count" data-filter="suggestion"><span class="wrn-dot suggestion"></span><span data-suggestions>0</span></button><button class="wrn-button" data-toggle>Inspect</button><button class="wrn-button" data-scan>↻</button></nav>';
|
||||
root.appendChild(shell);
|
||||
const panel=root.querySelector(".wrn-panel"), list=root.querySelector(".wrn-list"), meta=root.querySelector(".wrn-panel-meta"), search=root.querySelector(".wrn-search");
|
||||
const esc = value => String(value ?? "").replace(/[&<>\"']/g, char => ({"&":"&","<":"<",">":">",'"':""","'":"'"})[char]);
|
||||
const selector = el => { if (!el || el.nodeType !== 1) return ""; if (el.id) return "#"+CSS.escape(el.id); const parts=[]; let cur=el; while(cur && cur!==document.documentElement && parts.length<5){ let part=cur.tagName.toLowerCase(); const cls=[...cur.classList].slice(0,2).map(x=>"."+CSS.escape(x)).join(""); part+=cls; const parent=cur.parentElement; if(parent){ const same=[...parent.children].filter(x=>x.tagName===cur.tagName); if(same.length>1) part+=":nth-of-type("+(same.indexOf(cur)+1)+")"; } parts.unshift(part); cur=parent; } return parts.join(" > "); };
|
||||
const issue = (ruleId,category,severity,title,message,el,recommendation,confidence="high",metadata={}) => { const sel=selector(el); const source=el?.getAttribute?.("data-wrnexus-source")||""; return { id:"wrn-"+Date.now()+"-"+Math.random().toString(36).slice(2), ruleId,category,severity,title,message,recommendation,confidence,target:el?{selector:sel,tagName:el.tagName.toLowerCase(),text:(el.textContent||"").trim().slice(0,120)}:undefined,source:source?{file:source}:undefined,metadata,fingerprint:[ruleId,location.pathname,sel,source,message].join("|"),createdAt:Date.now() }; };
|
||||
const accessibleName = el => { const by=el.getAttribute("aria-labelledby"); if(by){ const t=by.split(/\s+/).map(id=>document.getElementById(id)?.textContent?.trim()||"").join(" ").trim(); if(t)return t; } return el.getAttribute("aria-label")?.trim()||el.getAttribute("alt")?.trim()||el.getAttribute("title")?.trim()||el.textContent?.trim()||""; };
|
||||
const visible = el => { const s=getComputedStyle(el),r=el.getBoundingClientRect(); return s.display!=="none"&&s.visibility!=="hidden"&&Number(s.opacity)!==0&&r.width>0&&r.height>0; };
|
||||
const scan = async () => {
|
||||
const found=[]; const q=s=>[...document.querySelectorAll(s)].filter(el=>!el.closest("wrnexus-dev-toolbar")&&!el.closest("[data-dev-toolbar-ignore='all']"));
|
||||
q("img:not([alt])").forEach(el=>found.push(issue("a11y/image-alt","accessibility","error","Image is missing alt text","Add alt text for meaningful images, or alt=\"\" for decorative images.",el,"Add a concise alt attribute.")));
|
||||
q("button,a[href],[role='button']").filter(el=>!accessibleName(el)).forEach(el=>found.push(issue("a11y/control-name","accessibility","error","Control has no accessible name","Screen readers cannot identify this control.",el,"Add visible text, aria-label, or aria-labelledby.")));
|
||||
q("input:not([type='hidden']),select,textarea").filter(el=>{const id=el.id;return !el.getAttribute("aria-label")&&!el.getAttribute("aria-labelledby")&&!(id&&document.querySelector("label[for='"+CSS.escape(id)+"']"))&&!el.closest("label")}).forEach(el=>found.push(issue("a11y/form-label","accessibility","error","Form control has no label","Users may not know what this field is for.",el,"Associate a visible label.")));
|
||||
const ids=new Set(); q("[id]").forEach(el=>{if(ids.has(el.id))found.push(issue("a11y/duplicate-id","accessibility","error","Duplicate element ID","The ID “"+el.id+"” is used more than once.",el,"Use a unique ID.")); else ids.add(el.id)});
|
||||
let prev=0; q("h1,h2,h3,h4,h5,h6").forEach(el=>{const level=Number(el.tagName.slice(1));if(prev&&level>prev+1)found.push(issue("a11y/heading-order","accessibility","warning","Heading level is skipped","Heading jumps from h"+prev+" to h"+level+".",el,"Use a logical heading hierarchy."));prev=level});
|
||||
if(!document.title.trim()) found.push(issue("seo/title-missing","seo","error","Page title is missing","The document does not have a useful title.",null,"Add a unique title."));
|
||||
if(!document.querySelector('meta[name="description"]')) found.push(issue("seo/description-missing","seo","warning","Meta description is missing","Search and social previews may lack a useful description.",null,"Add a unique meta description."));
|
||||
if(!document.querySelector('meta[name="viewport"]')) found.push(issue("seo/viewport-missing","seo","error","Viewport meta tag is missing","The page may render incorrectly on mobile devices.",null,"Add width=device-width, initial-scale=1."));
|
||||
if(!document.querySelector("h1")) found.push(issue("seo/h1-missing","seo","warning","Page has no h1 heading","The page lacks a clear primary heading.",null,"Add one descriptive h1."));
|
||||
q("img").forEach(el=>{const r=el.getBoundingClientRect();if(el.complete&&el.naturalWidth===0)found.push(issue("images/broken","images","error","Image failed to load",el.currentSrc||el.src||"Empty image source",el,"Verify the URL and content type."));if(!el.hasAttribute("width")&&!el.hasAttribute("height")&&getComputedStyle(el).aspectRatio==="auto")found.push(issue("images/dimensions","images","warning","Image has no reserved dimensions","This image may cause layout shift.",el,"Add width and height or CSS aspect-ratio."));if(visible(el)&&el.naturalWidth>0&&r.width>el.naturalWidth*1.25)found.push(issue("images/upscaled","images","warning","Image is being enlarged","Rendered width exceeds intrinsic width.",el,"Use a larger source image."));if(r.top>innerHeight*1.5&&el.loading!=="lazy")found.push(issue("images/lazy-below-fold","images","suggestion","Below-fold image is not lazy loaded","This image starts far below the viewport.",el,"Consider loading=\"lazy\".","medium"));});
|
||||
q("video").forEach(el=>{if(el.autoplay&&!el.muted)found.push(issue("media/autoplay-sound","media","error","Autoplay video is not muted","Autoplaying sound is disruptive and often blocked.",el,"Remove autoplay or add muted."));if(!el.querySelector('track[kind="captions"]'))found.push(issue("media/no-captions","media","warning","Video has no captions track","Spoken content may be inaccessible.",el,"Add a captions track when applicable."));});
|
||||
q("iframe:not([title])").forEach(el=>found.push(issue("media/iframe-title","media","error","Iframe is missing a title","Assistive technology cannot identify the embed.",el,"Add a concise title.")));
|
||||
const count=document.querySelectorAll("*").length;if(count>1500)found.push(issue("html/dom-size","html",count>3000?"error":"warning","DOM is large","The page contains "+count+" elements.",null,"Reduce wrappers and render hidden content on demand."));
|
||||
q("button button,button a,a a,a button").forEach(el=>found.push(issue("html/nested-interactive","html","error","Interactive elements are nested","Nested links or buttons create invalid interaction behavior.",el,"Use one interactive element.")));
|
||||
q("a").forEach(el=>{const href=el.getAttribute("href");if(!href||href==="#")found.push(issue("links/empty","links","warning","Link has no useful destination","The href is missing or only #.",el,"Use a real URL or a button."));if(el.target==="_blank"&&!/\b(noopener|noreferrer)\b/.test(el.rel))found.push(issue("links/blank-rel","security","warning","New-tab link lacks rel protection","The opened page may access window.opener.",el,"Add rel=\"noopener noreferrer\"."));});
|
||||
if(document.documentElement.scrollWidth>innerWidth+2)found.push(issue("responsive/document-overflow","responsive","error","Page has horizontal overflow","Document width exceeds the viewport.",null,"Inspect fixed widths, long text and overflowing media."));
|
||||
q("body *").filter(visible).slice(0,2500).forEach(el=>{const r=el.getBoundingClientRect();if((r.right>innerWidth+8||r.left<-8)&&found.filter(x=>x.ruleId==="responsive/element-overflow").length<20)found.push(issue("responsive/element-overflow","responsive","warning","Element extends outside the viewport","Element bounds exceed the current viewport.",el,"Use fluid sizing, wrapping, max-width or an intentional scroll container."));});
|
||||
const resources=performance.getEntriesByType("resource");const total=resources.reduce((s,e)=>s+(e.transferSize||0),0);if(resources.length>150)found.push(issue("performance/resource-count","performance","warning","Page loads many resources","Found "+resources.length+" resource requests.",null,"Remove duplicates and defer non-critical resources."));if(total>5000000)found.push(issue("performance/transfer-size","performance",total>10000000?"error":"warning","Page transfer size is large","Observed transfer size is about "+(total/1000000).toFixed(2)+" MB.",null,"Compress and optimize page resources."));
|
||||
const unique=new Map();[...state.issues.filter(x=>["runtime","network","server","compiler","routing"].includes(x.category)),...found].forEach(x=>unique.set(x.fingerprint,x));state.issues=[...unique.values()];
|
||||
state.report={url:location.href,pathname:location.pathname,title:document.title,status:200,generatedAt:Date.now(),issues:state.issues,metrics:{domNodes:count,cssResources:q('link[rel="stylesheet"]').length,scriptResources:q("script[src]").length,imageResources:q("img").length,totalTransferBytes:total,pageLoadMs:performance.getEntriesByType("navigation")[0]?.duration}}; render(); return state.report;
|
||||
};
|
||||
let highlightEl=null; const clearHighlight=()=>{highlightEl?.remove();highlightEl=null}; const highlight=sel=>{clearHighlight();let target;try{target=document.querySelector(sel)}catch{}if(!target)return;const r=target.getBoundingClientRect();highlightEl=document.createElement("div");highlightEl.className="wrn-highlight";Object.assign(highlightEl.style,{left:r.left+"px",top:r.top+"px",width:r.width+"px",height:r.height+"px"});root.appendChild(highlightEl);target.scrollIntoView({block:"center",behavior:"smooth"});setTimeout(clearHighlight,3000)};
|
||||
const render=()=>{const filtered=state.issues.filter(x=>!state.search||[x.title,x.message,x.ruleId,x.category,x.source?.file].join(" ").toLowerCase().includes(state.search));root.querySelector("[data-errors]").textContent=state.issues.filter(x=>x.severity==="error").length;root.querySelector("[data-warnings]").textContent=state.issues.filter(x=>x.severity==="warning").length;root.querySelector("[data-suggestions]").textContent=state.issues.filter(x=>x.severity==="suggestion").length;meta.textContent=location.pathname+" · "+state.issues.length+" issues";if(!filtered.length){list.innerHTML='<div class="wrn-empty">No matching issues. Run a fresh scan after changing the page.</div>';return;}list.innerHTML=filtered.map(x=>'<article class="wrn-issue"><span class="wrn-severity '+esc(x.severity)+'"></span><div><div class="wrn-issue-title">'+esc(x.title)+'</div><div class="wrn-issue-message">'+esc(x.message)+'</div><div class="wrn-issue-meta">'+esc(x.category)+' · '+esc(x.ruleId)+(x.source?.file?' · '+esc(x.source.file):'')+'</div></div><div class="wrn-actions">'+(x.target?.selector?'<button class="wrn-small" data-highlight="'+esc(x.target.selector)+'">Show</button>':'')+'</div></article>').join("");};
|
||||
const addRuntime=(severity,title,message,metadata={})=>{const x=issue("runtime/browser","runtime",severity,title,message,null,"Inspect the browser console and source stack.","high",metadata);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render()};
|
||||
const mergeServerIssues=issues=>{const unique=new Map(state.issues.map(x=>[x.fingerprint,x]));(issues||[]).forEach(x=>unique.set(x.fingerprint,x));state.issues=[...unique.values()];render()};
|
||||
const loadServerIssues=()=>fetch("/__wrnexus/dev-toolbar/issues?pathname="+encodeURIComponent(location.pathname),{headers:{accept:"application/json"}}).then(r=>r.ok?r.json():{issues:[]}).then(data=>mergeServerIssues(data.issues)).catch(()=>{});
|
||||
addEventListener("error",event=>{const target=event.target;if(target&&target!==window&&target.tagName){addRuntime("error","Resource failed to load",target.src||target.href||target.currentSrc||target.tagName,{tag:target.tagName});}else addRuntime("error",event.message||"Uncaught browser error",event.filename?event.filename+":"+event.lineno+":"+event.colno:"",{stack:event.error?.stack});},true);
|
||||
addEventListener("unhandledrejection",event=>addRuntime("error","Unhandled promise rejection",event.reason?.message||String(event.reason),{stack:event.reason?.stack}));
|
||||
root.addEventListener("click",event=>{const button=event.target.closest("button");if(!button)return;if(button.matches("[data-toggle]")){state.open=!state.open;panel.classList.toggle("open",state.open)}if(button.matches("[data-close]")){state.open=false;panel.classList.remove("open")}if(button.matches("[data-scan]"))scan();if(button.matches("[data-clear]")){state.issues=[];render()}if(button.dataset.filter){state.search=button.dataset.filter;search.value=state.search;state.open=true;panel.classList.add("open");render()}if(button.dataset.highlight)highlight(button.dataset.highlight)});
|
||||
search.addEventListener("input",()=>{state.search=search.value.toLowerCase();render()});
|
||||
addEventListener("wrnexus:navigated",()=>{if(state.config.scanOnNavigation)setTimeout(()=>{loadServerIssues();scan()},50)});addEventListener("wrnexus:hmr",()=>{if(state.config.scanOnHmr)setTimeout(()=>{loadServerIssues();scan()},100)});addEventListener("wrnexus:toolbar-message",event=>{const message=event.detail||{};if(message.type==="toolbar:issues")mergeServerIssues(message.issues);if(message.type==="toolbar:scan"&&state.config.scanOnHmr)setTimeout(scan,50)});addEventListener("wrnexus:runtime-error",event=>addRuntime("error","WRNexus runtime error",event.detail?.message||"Runtime failure",event.detail||{}));
|
||||
window.__wrnexusDevToolbar={open(){state.open=true;panel.classList.add("open")},close(){state.open=false;panel.classList.remove("open")},toggle(){state.open=!state.open;panel.classList.toggle("open",state.open)},scan,clear(){state.issues=[];render()},report(){return state.report},highlight,configure(config){state.config=Object.assign(state.config,config||{});shell.dataset.position=state.config.position;try{localStorage.setItem(KEY,JSON.stringify(state.config))}catch{}}};
|
||||
const observer=new MutationObserver(()=>{clearTimeout(observer.timer);observer.timer=setTimeout(()=>{if(!state.open)return;scan()},400)});observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style","src","href","alt","aria-label"]});
|
||||
setTimeout(()=>{loadServerIssues();scan()},100);
|
||||
})();`;
|
||||
Reference in New Issue
Block a user