agentleFS
AleksandarBisevacAleksandarBisevac/claude-plugins/.claude/skills/writing-javascript/SKILL.md

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.

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
  1. Distinctive:

    A render function takes state and returns a node: Do not have it also fetch, also persist, and also toast.

    line 70
  2. Distinctive:

    Every top-level name in the concatenated script shares one global scope: the panel's entire top level, across every part, is in it.

    line 79
  3. Distinctive:

    Prefer event delegation on a container over a listener per element.

    line 149

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 ' ')"
done

Both 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/reduce when the result is a value; a for loop when the point is a side effect. Do not force either.
  • Build DOM with the el() helper, not createElement + appendChild. It takes (tag, attrs, ...children) and handles class, 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 -l

    Hand-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.
  • const by default, let when it is reassigned, var never.
  • 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: manifestFindingsBox is named that way only because a second findingsBox would 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: search ui/shared/ before adding a helper, and run node tools/find-shared-candidates.mjs to scout for repeated logic.

    refactoring-the-assembled-ui has 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 in ui/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 to shared/; centralising the report's wiring is what would move it.

Readability

  • Name handlers for what happens, not for the event: clearFilters, not onClick.
  • 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 / *Helper names.

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

  • innerHTML with anything derived from the manifest. Escaping is systematic on the Python side; do not open a new hole on the client. Use textContent, or the el() builder.
  • Implicit globals — always declare. One stray assignment lands in the shared scope.
  • var, and relying on hoisting. Every top-level const/let is 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() in report.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, :target for 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 , <iframe or url(http.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.