writing-javascript
Skill4 starsChanged 3 months agoMITView on GitHub
How browser JavaScript is written in this repo — both surfaces now speak one modern dialect and this says what that means for the code you type, where ES modules are impossible and where they are merely unused, and how to pick a feature (Baseline plus a file:// gate). Covers functional idiom over imperative DOM work, the shared helper layer, modular structure without a bundler, DRY across the report and the panel, keeping features isolated so one failure cannot blank a page, readability, testability through the black-box browser gates, and the anti-patterns that have already shipped bugs here. Use when planning, writing, reading, reviewing or refactoring anything under scripts/ui/report/ or scripts/ui/panel/, when adding a UI behaviour, or when a helper is about to exist twice.
In this repository12 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- before-you-claimSkill
- choosing-what-to-optimizeSkill
- grill-meSkill
- no-silent-passSkill
- orchestrating-parallel-workSkill
- refactoring-the-assembled-uiSkill
- reproducing-ci-locallySkill
- running-resumable-sync-jobsSkill
- verifying-external-behaviorSkill
- writing-cssSkill
- writing-pythonSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 70
A render function takes state and returns a node: Do not have it also fetch, also persist, and also toast.
- Distinctive: line 79
Every top-level name in the concatenated script shares one global scope: the panel's entire top level, across every part, is in it.
- Distinctive: line 149
Prefer event delegation on a container over a listener per element.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing JavaScript here
There is no bundler, no npm, no TypeScript and no build step. The ordered parts under
ui/report/ and ui/panel/ are concatenated by Python into exactly one inline <script> per
page. The mechanics of that — ordering, the byte pins, splitting — belong to
refactoring-the-assembled-ui. This file is about the code you type.
Which dialect
Modern ES, in both — and both surfaces now speak it. This section used to carry a table
showing report.js as strictly ES5 against a modern panel.js; that gap was closed, and the
table went on describing it. Re-derive rather than reading a number here:
for d in report panel; do
printf '%s: var=%s function()=%s\n' "$d" \
"$(grep -rho '\bvar ' plugins/audit/scripts/ui/$d/ | wc -l | tr -d ' ')" \
"$(grep -rho 'function *(' plugins/audit/scripts/ui/$d/ | wc -l | tr -d ' ')"
doneBoth are at zero var and zero function (). What is NOT yet symmetric is JSDoc — the report's
parts carry a block per exported behaviour and the panel's parts carry almost none — and since
this project has no TypeScript, JSDoc is the type system it gets. Types must be specific
({HTMLTableRowElement}, {(a: string, b: string) => number}), never {Object}; when the type
and the code disagree, fix the CODE.
let-const has been Baseline widely available since 2019-03-20, and every other modern
construct this code would use is in the same bracket. Check before assuming, though —
grep '^| let-const ' in
.claude/skills/refactoring-the-assembled-ui/references/baseline-snapshot.md.
Two gates before using any feature. Baseline: widely = free, newly = feature-detect with a
fallback, limited = no. file://: Baseline measures engines, not schemes — js-modules is
widely available since 2020 and still impossible here, and localStorage is widely available and
inconsistent from disk (which is why every storage call in the tree is already wrapped in
try/catch). A feature must pass both.
ES modules: not available in the report, and available-but-unused in the panel. Do not
compress this into "modules are impossible here", which is what the previous version of this line
said and what kept the question closed. In the REPORT they are impossible: the block IS
type="module" — a module scope is the point — but import, export and dynamic import() all
need loading, and loading is what an opaque file:// origin refuses. The PANEL is served over
http://127.0.0.1, where a cross-file import works; it is measured, with a file:// control in
the same run failing as the report does. Writing one still needs a static route in
panel-server.py and a new home for the __*__ placeholders, so until that lands the panel's
parts contain no import either — but the reason is a missing route, not the platform. See
refactoring-the-assembled-ui for what adopting it would cost.
Functional idiom
panel.js already reads this way and it is the target for both surfaces.
map/filter/reducewhen the result is a value; aforloop when the point is a side effect. Do not force either.Build DOM with the
el()helper, notcreateElement+appendChild. It takes(tag, attrs, ...children)and handlesclass,on*handlers and text nodes; it is the panel's builder at several hundred call sites, and a count belongs in a shell rather than here:grep -rohE '\bel\(' plugins/audit/scripts/ui/panel/ | wc -lHand-rolled DOM assembly is longer, easier to get wrong, and cannot be shared.
- A render function takes state and returns a node. Do not have it also fetch, also persist, and also toast. The panel's six tab renderers are where this has drifted — seven functions over 100 lines account for a third of that file — so a new one should not join them.
constby default,letwhen it is reassigned,varnever.- No classes. There are zero in either file and no reason to introduce the first.
- Do not mutate a caller's object. Return a new one, the way the Python side does.
Structure, and the shared layer
Every top-level name in the concatenated script shares one global scope: the panel's entire
top level, across every part, is in it. The advice "module scope encapsulates" is false here — the
panel gets one classic <script>, so the concatenation IS the scope, and the report's module scope
covers its own parts and nothing about collisions between them.
- Prefix by feature, as the panel already does (
u*usage,p*policy,t*theme,ov*overview). The file itself records the near-miss this prevents:manifestFindingsBoxis named that way only because a secondfindingsBoxwould have hoisted over the first and broken every config save. - Group by feature, not by artifact.
report.* / panel.*is a split by output file, which is exactly why the same feature exists twice. Anything both surfaces need goes in
ui/shared/, once — and before writing a helper, ask whether it is already there. Two commands, the same two that answer "does this exist?":ls plugins/audit/scripts/ui/shared/ grep -rnE '^\s{0,2}(function|const|let) NAME\b' plugins/audit/scripts/ui/Nobody ran them for a long time, and the reason is worth knowing: the instruction named a directory that did not exist, so there was nothing to find and no cheap way to comply. What that allowed was measured and is now mostly repaid — the blob downloaders, the storage guards, the agreement rule for a count and its noun, the day-in-milliseconds constant, the clipboard's two failure paths, and the heatmap calendar that existed twice under the same names since both heatmaps did. The register that listed each concern with its home and an allowance (
_deps.SHARED_CONCERNS) was retired, so nothing enforces this: searchui/shared/before adding a helper, and runnode tools/find-shared-candidates.mjsto scout for repeated logic.refactoring-the-assembled-uihas the four-step wiring; the promotion rule is one reader stays put, two readers move up — and a shared part may not reach back into a surface, which is what decides how high a helper's own primitives have to sit.- Reach elements through dedicated
data-attributes, not styling classes. The hook is then explicit and greppable, and renaming a CSS class cannot silently break behaviour. Wrap each independent feature so its failure is contained. A report is opened from a CI artifact by someone who cannot fix it; one throwing feature must not blank the page:
On the panel, call
runContained— do not retype the loop. It lives inui/panel/core.js, takes named functions, and returns the names of the ones that threw so the caller decides who hears about it:const broke = runContained([themeToggle, filterBar, heatmap]); if (broke.length) toast('these are not up: ' + broke.join(', '), 'err');Pass named functions: an inline arrow has no name and reports as
(anonymous). The report has no central registration point yet, so it has no second reader and the helper has not been promoted toshared/; centralising the report's wiring is what would move it.
Readability
- Name handlers for what happens, not for the event:
clearFilters, notonClick. - Extract a multi-statement callback into a named function; keep inline callbacks to one expression.
- Name the predicate rather than inlining a three-term condition.
- Comment the constraint, not the mechanism — the comments worth keeping in this tree say what broke once.
- No
*Utils/*Helpernames.
Testability
The real test is a browser. tools/check-report-interactive.mjs drives the rendered report over
file:// and never opens the source — which is what makes it trustworthy, and what makes an
order-preserving change provably behaviour-free. tools/capture-screenshots.mjs --check drives
the panel.
So: write behaviour that is observable from the DOM. A feature whose only evidence is an
internal variable cannot be checked by either gate. State that a reader can see — a class, an
aria-pressed, a row count, a data- attribute — is state a test can assert.
Prefer event delegation on a container over a listener per element. The report does this deliberately for 1000+ marks; the panel re-derived the same conclusion locally for a 168-cell grid rather than adopting the mechanism.
Anti-patterns and pitfalls
innerHTMLwith anything derived from the manifest. Escaping is systematic on the Python side; do not open a new hole on the client. UsetextContent, or theel()builder.- Implicit globals — always declare. One stray assignment lands in the shared scope.
var, and relying on hoisting. Every top-levelconst/letis in TDZ until its line runs, which is also why part order is load-bearing.- A listener per element at scale.
Date.now()/new Date()inreport.js— pinned as forbidden, because a wall-clock call makes the rendered artifact non-reproducible.- Two implementations of one number. The token formatters already disagree:
uTok(2.6)gives"3",fmtTokens(2.6, 1)gives"2", and both claim to mirror the same Python. - Adding a feature the platform already has.
<details>/<summary>for disclosure,<dialog>for modals,:targetfor deep links. Deleted JavaScript is the cheapest JavaScript. - Loading anything external — no CDN, no font, no image URL. CI asserts the report contains
no
<script src,<img,<link,<iframeorurl(http.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

