Web Inspector Style Guide¶
This document covers coding conventions specific to Web Inspector (Source/WebInspectorUI/). For general WebKit coding style (C++, Objective-C), see the WebKit Code Style Guidelines.
Quick Reference¶
- COMPATIBILITY Comments -- version-gated compatibility guards for older Inspector backends
Top Code Style Issues¶
The rules most likely to come up in code review:
- Next-line braces for methods and top-level functions. Same-line braces for
if/for/while/switch, nested functions, and arrow functions. - Views never call protocol agents. Only Managers (
Controllers/) and some Models talk to the backend. _handleprefix for event handlers, noton. Theonprefix is not used.letis the default, even if the variable is never reassigned.constis only for named constant parameters and true constants.- Spell things out.
identifiernotid,elementnotel. Abbreviations are avoided. - Always pass
thistoWI.ObjectaddEventListener. The three-argument formaddEventListener(eventType, listener, thisObject)is required. Omittingthistriggers aconsole.assertin debug builds. (This applies toWI.Object's event system, not DOMEventTarget.)
Formatting¶
Indentation¶
4 spaces. No tabs. No trailing whitespace. Applies to both .js and .css files.
Strings¶
Use double quotes for all string literals. Single quotes are not used.
let name = "network-manager";
this.element.classList.add("content-view");
Use WI.UIString() with .format() for user-visible interpolated strings. Template literals are acceptable for non-localized strings (debug output, CSS generation, technical strings):
// Localized strings: .format()
WI.UIString("Import (%s)").format(WI.saveKeyboardShortcut.displayName);
// Non-localized: template literals are fine
styleText += `.show-whitespace-characters .CodeMirror .cm-whitespace-${count}::before {`;
Braces¶
Named functions and class methods -- opening brace on the next line:
WI.Object = class WebInspectorObject
{
static addEventListener(eventType, listener, thisObject)
{
// ...
}
};
Nested functions and arrow functions -- opening brace on the same line (K&R style):
class Foo {
bar()
{
function nested() {
/* ... */
}
this.baz(() => {
/* ... */
});
}
}
Control flow (if, for, while, switch) -- opening brace on the same line:
if (target.hasDomain("Network")) {
target.NetworkAgent.enable();
}
for (let item of this._items) {
item.reset();
}
Single-statement bodies may omit braces:
if (!supported)
continue;
Semicolons¶
Always use semicolons. No reliance on Automatic Semicolon Insertion.
Trailing Commas¶
Use trailing commas in all multi-line object and array literals:
WI.DOMManager.Event = {
AttributeModified: "dom-manager-attribute-modified",
AttributeRemoved: "dom-manager-attribute-removed",
InspectedNodeChanged: "dom-manager-inspected-node-changed",
};
Variable Declarations¶
Use let for all local variables. Do not use var in new code.
Use const only for named constant parameters and true constants -- values whose constancy is semantically important:
// Good: const for self-documenting boolean arguments
const shouldGroupNonExclusiveItems = true;
this._scopeBar = new WI.ScopeBar("filter", items, items[0], shouldGroupNonExclusiveItems);
// Good: let for everything else, even if never reassigned
let target = WI.assumingMainTarget();
let listenersForEventType = this._listeners.get(eventType);
This is the opposite of the const-by-default convention common in many JS projects.
Constructor Calls Without Arguments¶
Omit parentheses for no-argument constructors:
this._frameIdentifierMap = new Map;
this._downloadingSourceMaps = new Set;
Naming Conventions¶
Classes¶
Almost all classes live on the WI namespace. The class expression name mirrors the property name:
WI.NetworkManager = class NetworkManager extends WI.Object
{
Exceptions: generic utilities (Multimap, Debouncer) and Worker-only classes (HTMLParser, JSFormatter) live at the top level.
Properties and Methods¶
- Spell out full words:
identifiernotid,representedObjectnotrepObj. - Boolean getters use
isprefix:get isAttached(),get isClosed(). - Private members use underscore prefix:
this._mainFrame,_resetCollection(). - Event handlers use
_handleprefix:_handleFrameMainResourceDidChange(event).
Localized Strings¶
Wrap user-visible strings in WI.UIString(). Wrap intentionally-unlocalized strings in WI.unlocalizedString():
WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName);
WI.unlocalizedString("css");
Class Structure¶
Classes follow a strict section ordering using comment headers:
WI.ExampleManager = class ExampleManager extends WI.Object
{
constructor()
{
super();
this._items = new Map;
}
// Static
static supportsFeature()
{
return InspectorBackend.hasCommand("Example.doThing");
}
// Target
initializeTarget(target)
{
// Per-target protocol setup (Manager classes only).
}
// Public
get items() { return this._items; }
addItem(item)
{
// ...
}
// Protected
protectedMethod()
{
// For subclass use only.
}
// Private
_handleItemChanged(event)
{
// ...
}
};
The section order is: constructor, // Static, // Target, // Public, observer/delegate sections, // Protected, // Private.
Common section headers include:
// Static
// Target
// Public
// NetworkObserver
// Table delegate
// Protected (GeneralTreeElement)
// Protected
// Private
When overriding a superclass method, annotate the section: // Protected (ClassName). Always invoke super in overrides unless there is a specific reason not to.
Inline Getters¶
Simple getters that return a backing property go on one line:
get element() { return this._element; }
get layoutPending() { return this._dirty; }
get isAttached() { return this._isAttachedToRoot; }
Abstract Methods¶
Use WI.NotImplementedError.subclassMustOverride() for methods subclasses must implement:
get displayName()
{
throw WI.NotImplementedError.subclassMustOverride();
}
Architecture¶
Directory Structure¶
| Directory | Contents |
|---|---|
Base/ |
Core utilities (WI.Object, settings, URL/DOM utilities) |
Controllers/ |
Manager singletons (own protocol state and domain logic) |
Models/ |
Data model classes (Resource, Script, DOMNode) |
Views/ |
UI classes, each paired with a .css file |
Protocol/ |
Protocol observer dispatchers and target classes |
External/ |
Third-party code (CodeMirror, Esprima) -- exempt from these rules |
The Protocol Firewall¶
Protocol Agent <--> Observer <--> Manager <--> Model / View
(backend) (Protocol/) (Controllers/) (Models/ + Views/)
- Observers receive backend events and forward to Managers. They are thin dispatchers with no logic.
- Managers are the primary layer that calls protocol commands (
target.FooAgent.method()). Some Model classes also call protocol agents directly. - Models hold data and fire
WI.Objectevents. - Views listen to Model/Manager events and update DOM. They never call protocol agents directly.
View Lifecycle¶
Views extend WI.View. Key lifecycle methods:
initialLayout()-- Called once when first shown. Create complex DOM here, not in constructor.layout()-- Called when the view needs updating. Request vianeedsLayout(). ChecklayoutReasonforWI.View.LayoutReason.Resizeif only resize handling is needed.didLayoutSubtree()-- Called afterlayout()completes for the entire subtree.sizeDidChange()-- Called when the view's size changes.attached()/detached()-- Called when entering/leaving the view hierarchy. Add event listeners inattached(), remove indetached().
attached()
{
super.attached();
WI.networkManager.addEventListener(WI.NetworkManager.Event.FrameWasAdded, this._handleFrameWasAdded, this);
}
detached()
{
WI.networkManager.removeEventListener(WI.NetworkManager.Event.FrameWasAdded, this._handleFrameWasAdded, this);
super.detached();
}
Event System¶
Declaring Events¶
Events are declared as a static Event property outside the class body. Keys are PascalCase; values are hyphenated-lowercase prefixed with the class name:
WI.NetworkManager.Event = {
FrameWasAdded: "network-manager-frame-was-added",
MainFrameDidChange: "network-manager-main-frame-did-change",
};
Use string literals for enumeration values, not Symbol(). Symbol is not cheap to create, and the uniqueness guarantee is rarely needed for enums. Reserve Symbol for unique identifiers in loops (e.g., promiseIdentifier in Debouncer) or expando properties on objects (e.g., WI.TabBrowser.NeedsResizeLayoutSymbol).
// Do -- string literals for enums
WI.Resource.ResponseSource = {
Unknown: "unknown",
Network: "network",
MemoryCache: "memory-cache",
};
// Don't -- Symbol is unnecessary overhead for enums
WI.Resource.ResponseSource = {
Unknown: Symbol("unknown"),
Network: Symbol("network"),
MemoryCache: Symbol("memory-cache"),
};
Listening and Dispatching (WI.Object)¶
The following applies to WI.Object's custom event system, not the DOM EventTarget API.
Use the three-argument addEventListener(eventType, listener, thisObject). Always pass this as the third argument -- it enables proper cleanup and this-binding:
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
Dispatch with dispatchEventToListeners:
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name});
Listen on a class constructor to hear events from all instances. Listen on a specific instance for just that object.
One-Shot and Async Events¶
// Listen once
WI.Target.singleFireEventListener(WI.Target.Event.Removed, this._targetRemoved, this);
// Await as a Promise (thisObject required)
await WI.DOMManager.awaitEvent(WI.DOMManager.Event.DocumentUpdated, this);
Global Notifications¶
For application-wide events, use WI.notifications:
WI.notifications.addEventListener(WI.Notification.GlobalModifierKeysDidChange, this._handleModifierKeysChanged, this);
Collections and Iteration¶
Prefer Map and Set over plain objects for dynamic keys:
this._frameIdentifierMap = new Map;
this._downloadingSourceMaps = new Set;
Prefer for...of for iteration:
for (let override of serializedOverrides) {
let localResourceOverride = WI.LocalResourceOverride.fromJSON(override);
// ...
}
Arrow Functions¶
Use arrow functions for callbacks. Always parenthesize parameters, even single ones:
// Do
let results = listenersForEventType.filter((item) => item.listener === listener);
// Don't
let results = listenersForEventType.filter(item => item.listener === listener);
Use single-line arrow functions only when using the return value (e.g., .filter(), .map()). For side-effect-only callbacks, use multi-line form:
// Do -- multi-line for side effects
target.DOMAgent.getDocument()
.then((result) => {
/* ... */
})
.catch((error) => {
WI.reportInternalError(error);
});
// Don't -- single-line for side effects
target.DOMAgent.getDocument()
.then((result) => doSomething(result))
.catch((error) => WI.reportInternalError(error));
Async Patterns¶
Both async/await and .then() chaining exist. Prefer async/await for new code:
WI.Target.registerInitializationPromise((async () => {
let serialized = await WI.objectStores.localResourceOverrides.getAll();
for (let entry of serialized) {
// ...
}
})());
Promise Gotchas¶
Always add a .catch() to promise chains. Dropped promise rejections are silent and cause hard-to-debug failures. Most promise chains should have error handling:
// Do
target.DOMAgent.getDocument()
.then((result) => { /* ... */ })
.catch((error) => { WI.reportInternalError(error); });
// Don't -- rejection silently disappears
target.DOMAgent.getDocument()
.then((result) => { /* ... */ });
Chain promises -- don't nest them. Nested .then() calls create "promise pyramids" that are hard to read and easy to break:
// Do -- flat chain
fetchA()
.then((a) => fetchB(a))
.then((b) => process(b))
.catch(handleError);
// Don't -- nested pyramid
fetchA().then((a) => {
fetchB(a).then((b) => {
process(b);
});
});
Return promises from .then() callbacks. Forgetting to return breaks the chain -- subsequent .then() calls receive undefined instead of the result:
// Do -- return the inner promise
.then((result) => {
return target.RuntimeAgent.evaluate.invoke({expression: "1+1"});
})
// Don't -- chain is broken, next .then() gets undefined
.then((result) => {
target.RuntimeAgent.evaluate.invoke({expression: "1+1"});
})
Use Promise.all() for parallel work, not sequential .then() chains:
// Do -- parallel
let [scripts, stylesheets] = await Promise.all([
fetchScripts(),
fetchStylesheets(),
]);
// Don't -- needlessly sequential
let scripts = await fetchScripts();
let stylesheets = await fetchStylesheets();
Wrap async IIFEs when registering initialization promises:
WI.Target.registerInitializationPromise((async () => {
// async work here
})());
Note the double parentheses: (async () => { ... })() -- the IIFE is invoked immediately and the resulting promise is passed to registerInitializationPromise.
Assertions¶
Use console.assert() liberally. Pass relevant values after the condition for debugging:
console.assert(target instanceof WI.Target, target);
console.assert(!disabled || typeof disabled === "boolean", disabled);
CSS Conventions¶
Z-Index¶
Never use raw z-index numbers. Use custom properties from Views/Variables.css:
:root {
--z-index-highlight: 64;
--z-index-header: 128;
--z-index-resizer: 256;
--z-index-popover: 512;
--z-index-tooltip: 1024;
--z-index-glass-pane-for-drag: 2048;
--z-index-uncaught-exception-sheet: 4096;
}
Colors and Theming¶
Use semantic color custom properties from Variables.css. Dark mode is handled by redefining properties in @media (prefers-color-scheme: dark) blocks:
.my-view {
color: var(--text-color);
background-color: var(--background-color-content);
}
CSS Class Names¶
Use hyphenated-lowercase names. Each View has a paired CSS file with the same base name (NetworkTableContentView.js / NetworkTableContentView.css).