release: WRNexusJS 0.6.0

This commit is contained in:
2026-08-01 01:09:58 +05:30
parent 3e565e8d03
commit 687d345882
502 changed files with 33038 additions and 11358 deletions
+12 -5
View File
@@ -1,7 +1,7 @@
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:"", severity:"all", config:{ position:"bottom-center", scanOnNavigation:true, scanOnHmr:true } };
const state = { open:false, issues:[], report:null, search:"", severity:"all", category:"all", platform:null, panels:[], 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", "");
@@ -11,7 +11,7 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => {
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>';
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-category="all">Issues</button><button class="wrn-small" data-category="runtime">Runtime</button><button class="wrn-small" data-category="stores">Stores</button><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 => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[char]);
@@ -39,18 +39,25 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => {
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()];
q("[data-wrn-client-module]").forEach(el=>found.push(issue("runtime/client-module","runtime","info","Client function module",el.getAttribute("data-wrn-client-module")||"Unknown module",el,"Loaded according to the component hydration strategy.","high",{hydration:el.getAttribute("data-wrn-hydrate"),runtime:el.getAttribute("data-wrn-runtime")})));
const storeContainer=window.__wrnexusStoreContainer;
if(storeContainer&&typeof storeContainer.inspect==="function"){
for(const store of storeContainer.inspect())found.push(issue("stores/instance","stores","info",store.kind+" store: "+store.name,JSON.stringify({state:store.state,computed:store.computed}),null,"Use store actions for mutations. Sensitive server state is never hydrated.","high",store));
}
const unique=new Map();[...state.issues.filter(x=>["runtime","stores","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=>{const severityMatches=state.severity==="all"||x.severity===state.severity;const textMatches=!state.search||[x.title,x.message,x.ruleId,x.category,x.severity,x.source?.file].join(" ").toLowerCase().includes(state.search);return severityMatches&&textMatches;});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;root.querySelectorAll("[data-filter]").forEach(button=>{const active=button.dataset.filter===state.severity;button.classList.toggle("active",active);button.setAttribute("aria-pressed",String(active));});meta.textContent=location.pathname+" · "+state.issues.length+" issues"+(state.severity!=="all"?" · "+state.severity:"");if(!filtered.length){list.innerHTML='<div class="wrn-empty">No issues match the current filters.</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.severity)+' · '+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 render=()=>{const filtered=state.issues.filter(x=>{const severityMatches=state.severity==="all"||x.severity===state.severity;const categoryMatches=state.category==="all"||x.category===state.category;const textMatches=!state.search||[x.title,x.message,x.ruleId,x.category,x.severity,x.source?.file].join(" ").toLowerCase().includes(state.search);return severityMatches&&categoryMatches&&textMatches;});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;root.querySelectorAll("[data-filter]").forEach(button=>{const active=button.dataset.filter===state.severity;button.classList.toggle("active",active);button.setAttribute("aria-pressed",String(active));});root.querySelectorAll("[data-category]").forEach(button=>{const active=button.dataset.category===state.category;button.classList.toggle("active",active);button.setAttribute("aria-pressed",String(active));});meta.textContent=location.pathname+" · "+state.issues.length+" findings"+(state.category!=="all"?" · "+state.category:"")+(state.severity!=="all"?" · "+state.severity:"");if(!filtered.length){list.innerHTML='<div class="wrn-empty">No issues match the current filters.</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.severity)+' · '+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()};
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;state.severity="all";panel.classList.toggle("open",state.open);render()}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=[];state.severity="all";state.search="";search.value="";render()}if(button.dataset.filter){state.severity=state.severity===button.dataset.filter?"all":button.dataset.filter;state.open=true;panel.classList.add("open");render()}if(button.dataset.highlight)highlight(button.dataset.highlight)});
root.addEventListener("click",event=>{const button=event.target.closest("button");if(!button)return;if(button.matches("[data-toggle]")){state.open=!state.open;state.severity="all";panel.classList.toggle("open",state.open);render()}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=[];state.severity="all";state.category="all";state.search="";search.value="";render()}if(button.dataset.category){state.category=button.dataset.category;state.open=true;panel.classList.add("open");render()}if(button.dataset.filter){state.severity=state.severity===button.dataset.filter?"all":button.dataset.filter;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(scan,50)});addEventListener("wrnexus:hmr",()=>{if(state.config.scanOnHmr)setTimeout(scan,100)});addEventListener("wrnexus:runtime-error",event=>addRuntime("error","WRNexus runtime error",event.detail?.message||"Runtime failure",event.detail||{}));
addEventListener("wrnexus:diagnostic",event=>{const detail=event.detail||{};const code=detail.code||"WRN-RUNTIME";const category=String(code).includes("HYDRATE")?"runtime":String(code).includes("ROUTE")?"routing":"compiler";const title=String(code).includes("HYDRATE")?"Hydration diagnostic":"WRNexus diagnostic";const x=issue(String(code),category,"error",title,detail.message||"Framework diagnostic",null,"Open the source location and resolve the reported framework contract.","high",detail);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();});
addEventListener("wrnexus:store-mutation",event=>{const mutation=event.detail||{};const x=issue("stores/mutation","stores","info","Store action: "+(mutation.store||"unknown")+"."+(mutation.action||"direct"),"Changed fields: "+((mutation.changed||[]).join(", ")||"none"),null,"Use the Stores panel to inspect safe client state.","high",mutation);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();});
fetch("/__wrnexus/dev-toolbar/platform").then(r=>r.ok?r.json():null).then(data=>{if(!data)return;state.platform=data.platform;state.panels=data.panels||[];render();}).catch(()=>{});
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(scan,100);
+1 -1
View File
@@ -1,4 +1,4 @@
export const DEV_TOOLBAR_CSS = String.raw`
:host{all:initial;color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--bg:#111318;--panel:#181b22;--line:#2b303b;--text:#f4f6fb;--muted:#9ca6b8;--error:#ff6b6b;--warning:#ffc857;--info:#72a7ff;--suggestion:#a78bfa}
*{box-sizing:border-box}.wrn-shell{position:fixed;z-index:2147483646;bottom:16px;left:50%;transform:translateX(-50%);color:var(--text);font-size:13px;line-height:1.4}.wrn-shell[data-position="bottom-left"]{left:16px;transform:none}.wrn-shell[data-position="bottom-right"]{left:auto;right:16px;transform:none}.wrn-bar{display:flex;align-items:center;gap:6px;padding:7px;border:1px solid var(--line);border-radius:14px;background:rgba(17,19,24,.96);box-shadow:0 18px 60px rgba(0,0,0,.42);backdrop-filter:blur(18px)}button{appearance:none;border:0;font:inherit}.wrn-button,.wrn-count{display:inline-flex;align-items:center;justify-content:center;min-height:32px;border-radius:9px;padding:0 10px;background:#222631;color:var(--text);cursor:pointer}.wrn-button:hover,.wrn-count:hover{background:#2b303c}.wrn-count.active{background:#394150;box-shadow:inset 0 0 0 1px #596579}.wrn-button:focus-visible,.wrn-count:focus-visible{outline:2px solid #72a7ff;outline-offset:2px}.wrn-brand{font-weight:800;letter-spacing:-.02em;padding:0 8px}.wrn-count{gap:5px;font-variant-numeric:tabular-nums}.wrn-dot{width:7px;height:7px;border-radius:999px}.wrn-dot.error{background:var(--error)}.wrn-dot.warning{background:var(--warning)}.wrn-dot.suggestion{background:var(--suggestion)}.wrn-panel{position:absolute;bottom:50px;left:50%;transform:translateX(-50%);width:min(780px,calc(100vw - 24px));height:min(620px,calc(100vh - 100px));display:none;overflow:hidden;border:1px solid var(--line);border-radius:16px;background:var(--bg);box-shadow:0 24px 80px rgba(0,0,0,.5)}.wrn-panel.open{display:grid;grid-template-rows:auto auto 1fr}.wrn-panel-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line)}.wrn-panel-title{font-size:15px;font-weight:800}.wrn-panel-meta{color:var(--muted);font-size:12px}.wrn-toolbar-row{display:flex;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line)}.wrn-search{width:100%;height:34px;border:1px solid var(--line);border-radius:9px;background:#20242d;color:var(--text);padding:0 10px;outline:none}.wrn-search:focus{border-color:#72a7ff}.wrn-list{overflow:auto;padding:10px}.wrn-empty{display:grid;place-items:center;height:100%;color:var(--muted);text-align:center;padding:40px}.wrn-issue{display:grid;grid-template-columns:8px 1fr auto;gap:10px;padding:12px;margin-bottom:8px;border:1px solid var(--line);border-radius:12px;background:var(--panel)}.wrn-severity{border-radius:999px;background:var(--info)}.wrn-severity.error{background:var(--error)}.wrn-severity.warning{background:var(--warning)}.wrn-severity.suggestion{background:var(--suggestion)}.wrn-issue-title{font-weight:750}.wrn-issue-message{margin-top:4px;color:#c8cfdb}.wrn-issue-meta{margin-top:7px;color:var(--muted);font-size:11px}.wrn-actions{display:flex;gap:5px}.wrn-small{height:28px;padding:0 8px;border-radius:7px;background:#252a35;color:var(--text);cursor:pointer}.wrn-small:hover{background:#303644}.wrn-highlight{position:fixed;z-index:2147483647;pointer-events:none;border:2px solid #72a7ff;background:rgba(114,167,255,.12);box-shadow:0 0 0 99999px rgba(0,0,0,.08)}@media(max-width:640px){.wrn-shell{bottom:8px}.wrn-brand{display:none}.wrn-bar{gap:3px;padding:5px}.wrn-count{padding:0 7px}.wrn-panel{bottom:44px;height:calc(100vh - 62px)}.wrn-issue{grid-template-columns:6px 1fr}.wrn-actions{grid-column:2}.wrn-panel-meta{display:none}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
*{box-sizing:border-box}.wrn-shell{position:fixed;z-index:2147483646;bottom:16px;left:50%;transform:translateX(-50%);color:var(--text);font-size:13px;line-height:1.4}.wrn-shell[data-position="bottom-left"]{left:16px;transform:none}.wrn-shell[data-position="bottom-right"]{left:auto;right:16px;transform:none}.wrn-bar{display:flex;align-items:center;gap:6px;padding:7px;border:1px solid var(--line);border-radius:14px;background:rgba(17,19,24,.96);box-shadow:0 18px 60px rgba(0,0,0,.42);backdrop-filter:blur(18px)}button{appearance:none;border:0;font:inherit}.wrn-button,.wrn-count{display:inline-flex;align-items:center;justify-content:center;min-height:32px;border-radius:9px;padding:0 10px;background:#222631;color:var(--text);cursor:pointer}.wrn-button:hover,.wrn-count:hover{background:#2b303c}.wrn-count.active{background:#394150;box-shadow:inset 0 0 0 1px #596579}.wrn-button:focus-visible,.wrn-count:focus-visible{outline:2px solid #72a7ff;outline-offset:2px}.wrn-brand{font-weight:800;letter-spacing:-.02em;padding:0 8px}.wrn-count{gap:5px;font-variant-numeric:tabular-nums}.wrn-dot{width:7px;height:7px;border-radius:999px}.wrn-dot.error{background:var(--error)}.wrn-dot.warning{background:var(--warning)}.wrn-dot.suggestion{background:var(--suggestion)}.wrn-panel{position:absolute;bottom:50px;left:50%;transform:translateX(-50%);width:min(780px,calc(100vw - 24px));height:min(620px,calc(100vh - 100px));display:none;overflow:hidden;border:1px solid var(--line);border-radius:16px;background:var(--bg);box-shadow:0 24px 80px rgba(0,0,0,.5)}.wrn-panel.open{display:grid;grid-template-rows:auto auto 1fr}.wrn-panel-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line)}.wrn-panel-title{font-size:15px;font-weight:800}.wrn-panel-meta{color:var(--muted);font-size:12px}.wrn-toolbar-row{display:flex;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line)}.wrn-search{width:100%;height:34px;border:1px solid var(--line);border-radius:9px;background:#20242d;color:var(--text);padding:0 10px;outline:none}.wrn-search:focus{border-color:#72a7ff}.wrn-list{overflow:auto;padding:10px}.wrn-empty{display:grid;place-items:center;height:100%;color:var(--muted);text-align:center;padding:40px}.wrn-issue{display:grid;grid-template-columns:8px 1fr auto;gap:10px;padding:12px;margin-bottom:8px;border:1px solid var(--line);border-radius:12px;background:var(--panel)}.wrn-severity{border-radius:999px;background:var(--info)}.wrn-severity.error{background:var(--error)}.wrn-severity.warning{background:var(--warning)}.wrn-severity.suggestion{background:var(--suggestion)}.wrn-issue-title{font-weight:750}.wrn-issue-message{margin-top:4px;color:#c8cfdb}.wrn-issue-meta{margin-top:7px;color:var(--muted);font-size:11px}.wrn-actions{display:flex;gap:5px}.wrn-small{height:28px;padding:0 8px;border-radius:7px;background:#252a35;color:var(--text);cursor:pointer}.wrn-small:hover{background:#303644}.wrn-small.active{background:#394150;box-shadow:inset 0 0 0 1px #596579}.wrn-highlight{position:fixed;z-index:2147483647;pointer-events:none;border:2px solid #72a7ff;background:rgba(114,167,255,.12);box-shadow:0 0 0 99999px rgba(0,0,0,.08)}@media(max-width:640px){.wrn-shell{bottom:8px}.wrn-brand{display:none}.wrn-bar{gap:3px;padding:5px}.wrn-count{padding:0 7px}.wrn-panel{bottom:44px;height:calc(100vh - 62px)}.wrn-issue{grid-template-columns:6px 1fr}.wrn-actions{grid-column:2}.wrn-panel-meta{display:none}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
`;
+1
View File
@@ -2,6 +2,7 @@ export type DevToolbarSeverity = "error" | "warning" | "info" | "suggestion";
export type DevToolbarCategory =
| "runtime"
| "stores"
| "compiler"
| "server"
| "routing"