npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aemforms/eslint-plugin

v1.0.24

Published

ESLint rules for AEM Adaptive Forms design-canon + correctness checks — the JS-anchored subset of performance-bot's analyzers, sharing its matcher modules.

Readme

@aemforms/eslint-plugin

ESLint rules for AEM Adaptive Forms — they catch the mistakes in form JavaScript that cause runtime bugs, broken reuse, and data loss on resume, right in your editor and in CI.

They also make AI-assisted development safer: these rules encode the AEM Forms patterns that code generation most often gets wrong — async ordering, reusable-fragment scoping, and putting logic in code that belongs in the form model — so AI-written custom functions and rules are correct by construction, not just plausible-looking.

Install

npm i -D @aemforms/eslint-plugin eslint
// eslint.config.js  (flat config, ESLint 9 / 8.57+)
import aemForms from '@aemforms/eslint-plugin';

export default [
  aemForms.configs.recommended,
  // A few rules also cross-check your form's JSON on disk — point them at that folder:
  {
    files: ['**/*.js'],
    rules: {
      'aem-forms/storage-class':           ['error', { formJsonRoot: 'local-form-json' }],
      'aem-forms/fragment-path-validator': ['error', { formJsonRoot: 'local-form-json' }],
      'aem-forms/fragment-qualified-name': ['error', { formJsonRoot: 'local-form-json' }],
      'aem-forms/orphan-fragment-handler': ['error', { formJsonRoot: 'local-form-json' }],
    },
  },
];

Suppress one finding: // eslint-disable-next-line aem-forms/<rule>. To silence just one finding type of a multi-type rule (and keep the rest), use the ignoreTypes option: 'aem-forms/custom-fn-correctness': ['error', { ignoreTypes: ['jsonmodel-direct-read'] }] — the same option name and semantics work in both ESLint and the performance-bot CLI.

A little context (so the rules make sense)

An Adaptive Form is authored as data (fields, panels, rules) plus small pieces of JavaScript:

  • Custom functions / rule scripts — JS the form calls in response to a change, click, or event. They run headless (also on the server, in tests), so they must not touch the browser (window, the DOM) directly.
  • Rules — declarative expressions authored on a field (its value, whether it's visible/required, its validation). Logic that derives one value from others generally belongs in a rule, not in JS, because the form re-evaluates rules automatically (including when data is restored).
  • Fragments — reusable sub-forms embedded into larger forms. A fragment must only touch its own fields, addressed through globals.fragment, never the parent form's fields (globals.form), or it breaks when embedded somewhere else.
  • Resume / prefill — the form can be rehydrated with saved data (a user resumes, or data is prefilled). This replays change handlers without a real user interaction, which is the source of a whole class of bugs below.

Rules

Correctness — bugs that surface at runtime

| Rule | What it catches | |---|---| | async-value-race | A value fetched from an API and saved inside .then(...), but read elsewhere before the fetch finishes (so the reader gets the old value) — or a .catch that saves an empty value on failure, quietly losing data. | | request-error-handling | A request/api-client chain missing part or all of its error handling. An HTTP or network/connectivity failure RESOLVES { ok: false } — a .catch-only chain never sees it and the failure slips through as success (check res.ok too). A rejecting/malformed request payload genuinely rejects — a chain with neither a .catch nor an .ok check misses that case entirely, even through wrapper functions and await. See rule details. | | interactive-change-guard | A UI action (moving focus, auto-advancing to the next field) that runs on every change, including when saved data is restored on resume. On resume this jumps focus/steps around and corrupts the screen. Gate it to real user edits. | | custom-fn-correctness | An imperative setProperty that derives an editable property (a one-shot write that never recomputes — use field.bind), markFieldAsInvalid used imperatively (bind validationExpression instead), or reading _jsonModel directly (bypasses change tracking, so dependent rules won't re-run). See rule details. | | headless-incompatible-usage | Custom-fn-surface JS assuming a live browser runtime it shouldn't — DOM access, window access, a raw HTTP call, navigation, or view/keystroke DOM in an orchestration file. See rule details. | | custom-function | Direct $properties mutation, many single-field updates that should be one bulk update, or a custom event dispatched from a custom function. (Browser/window/DOM/HTTP access is headless-incompatible-usage, above.) | | navigation-in-custom-fn | No findings of its own — both of its checks are headless-incompatible-usage now. Kept registered so existing config referencing this id doesn't error. | | should-be-component | A field's own value being string-normalized (.toUpperCase(), .trim(), …) in orchestration code and written back as the submitted value — a keystroke concern that belongs in the field's component. See rule details. (View/keystroke DOM is headless-incompatible-usage, above.) |

Reuse — code that breaks when a fragment or component is embedded elsewhere

| Rule | What it catches | |---|---| | fragment-globals-scope | A fragment reaching into the parent form (globals.form.<field>) instead of its own scope (globals.fragment). Works in one form, breaks when reused in another. | | fragment-qualified-name | Inside a fragment, addressing its own fields by the parent-form path (globals.form.<fragmentName>.…) instead of globals.fragment.…. | | foreign-fragment-root | One fragment reading or writing another fragment's fields. Send an event instead and let the owning fragment update itself. | | field-writes-sibling | One field directly setting another field's value or options. Broadcast an event and let the target field fill itself, so neither field depends on the other's internals. | | dispatch-target | An event sent to a single field (only that field's children receive it) when it should go to the whole form — or sent as a plain object instead of a proper event. | | fragment-path-validator | A field path in the code (globals.fragment/form.<path>) that doesn't exist in the form's JSON — a typo or a rename that wasn't updated, which silently reads undefined at runtime. | | orphan-fragment-handler | An exported handler that no event or rule ever calls — dead code, usually a handler someone forgot to wire up. |

Model ownership — logic that belongs in the form, not in JavaScript

| Rule | What it catches | |---|---| | rule-vs-code | Deciding a field's value or visibility in JS (author it as a rule instead, so it re-evaluates automatically), or marking a field invalid imperatively (model it as the field's validation instead). | | storage-class | A saved variable with no data./uistate. prefix (so it's unclear whether it should survive a resume), form data copied into a separate variable, reading form state inside a startup handler, or persisting a value that's COMPUTED from other form state (re-derive it instead of storing it). | | content-in-code | User-facing text or error messages hardcoded in a function instead of authored on the form (where they can be translated and edited). | | display-format-in-code | Formatting a display value in code (e.g. adding a currency symbol). Author it as the field's display format, so it survives when data is restored. | | component-owns-model-concern | A custom component re-implementing something the form model already handles (formatting, validation, change events). | | component-value-sync | A component that copies the field value once but never updates when the value later changes — so prefilled or restored data is lost. | | component-model | A component defining a setting that duplicates a built-in one, or reading a setting the component never declares (so it's always empty). | | block-decorator-input-mutation | Changing an input's value in the browser (event.target.value = … in an input/keydown/keyup handler) without committing it to the form model, so the model and the screen disagree. Set the value on the MODEL instead (setProperty/setValue); if it represents a user change, dispatch a UIChange event so the runtime records it as user input (eventSource:'ui'). | | runtime-cls | Changing styles/classes while the form loads in a way that shifts the layout (a visible jump for the user). |

Auto-fixable: storage-class-namespace suggests data. / uistate. names.

Rule details

Every report message is self-contained — you shouldn't need this section to act on a finding. A few rules (below) have exceptions or several distinct checks that are worth spelling out in more depth than fits in a report message; that fuller version lives here. (Editors that support meta.docs.url, e.g. VS Code's ESLint extension, also surface this page as a "documentation" link on hover — a bonus for those editors, not the primary way findings are meant to be understood.)

rule-vs-code

Flags a function that only sets a computed editable property (no async/DOM/request/event-payload read) — that's a derived value, visibility, constraint, or label, which belongs in a fd:rules expression so it re-evaluates automatically, not in a change handler that has to be triggered and re-triggered by hand. "Editable property" is the full set of runtime-writable properties (value, visible, enabled, required, label, enum/enumNames, minimum/maximum, minLength/ maxLength, pattern, placeholder, description, …), derived from the runtime property matrix — not just value/visibility. Also flags markFieldAsInvalid used imperatively — validity belongs in the field's validationExpression (returns a boolean / {valid, errorMessage} / a Promise) so validate()/validateAsync()/submit pick it up.

Exception: keep it imperative if this is a one-shot seed of user-editable data — e.g. prefilling an address/name from a snapshot at init. That value is Class-1 dataRef data, restored via importData on resume and editable by the user; a value rule would reclassify it as derived and re-fire on every input change, overwriting the user's edit or the restored value on resume. If that's your case, this is a false positive — suppress it with // eslint-disable-next-line aem-forms/rule-vs-code.

This rule is the authoring-canon angle (prefer a content-authored fd:rules rule); a code-first team can turn it off. The same code shape is ALSO flagged by custom-fn-correctness from the correctness angle (use field.bind) — that finding stays on even when this rule is disabled.

custom-fn-correctness

AST-detectable custom-function correctness bugs — each a silent failure, all error:

  • non-reactive-set-use-bind — an imperative setProperty(target, { <editableProp>: <computed> }) is a ONE-SHOT write: it applies once and never recomputes when its inputs change. Use field.bind('<prop>', (globals) => …) so the property is bound to a reactive, dependency-tracked expression that re-fires on change — the code-path equivalent of an authored fd:rules rule. This is the same code shape rule-vs-code flags, but from the correctness angle: rule-vs-code says "prefer authoring a rule" (disable-able); this says "if you keep it in code, make it reactive" (stays on). Same escape hatches as rule-vs-code — a set that reads an event payload / does async / touches the DOM is a legitimate imperative fill and is not flagged, and a literal-constant set ({ visible: true }) is imperative setup, not a derived value.
  • markfieldasinvalid-in-fn-use-bindmarkFieldAsInvalid imperatively fights the validity slot; bind the field's validationExpression instead (field.bind('validationExpression', fn)), a reactive expression that validate()/submit pick up automatically.
  • jsonmodel-direct-read — reading _jsonModel.* bypasses the dependency-tracking getter, so a rule that reads it will NOT re-fire when the value changes. Read field.$value / the tracked getter instead.

(Note: field.parent and the async-custom-function check were removed — the af2-web-runtime rule node now resolves parent, and async functions register and run.)

content-in-code

User-facing copy — display text AND validation/error messages — belongs in authored content, not a string literal in a rule/fragment function. It flags any prose string (2+ real words; a single token or a formatting fragment like '₹ ' + num is not prose, so it won't trip this) reaching one of three sinks:

  • Returned directly from a function — return 'Please enter a valid amount';.
  • setProperty(field, { value: <prose> }) — display copy pushed straight into a field's value.
  • errorMessage: <prose> inside any object literal (not just a return's direct argument — the object is routinely the implicit return of an inner .catch() arrow, e.g. .catch(() => ({ valid: false, errorMessage: 'Bank not found.' })), so this is checked everywhere, not only where an explicit return sits).

The fix depends on which of the two you have:

  • Display text → author a dynamic-text field with ${key} placeholders (e.g. Hello ${firstName}, your loan amount is ₹${loanAmount}.) and set only the underlying data properties from code — never the finished sentence.
  • Validation/error message → author the field's constraintMessages / validateExpMessage, and return { valid: false } with no errorMessage — the runtime falls back to the authored message automatically.

Prose reaching a sink through a same-file const/let identifier — the common "hoist the copy to a module const" shape (const MSG = 'Please enter…'; return { valid:false, errorMessage: MSG }, including a props.x || DEFAULT_MSG fallback) — is flagged the same way as a direct literal; hoisting the string to a constant doesn't move it out of code.

Not flagged, by design: reading an authored message off a property (e.g. errorMessage: props.notServiceableMessage) — that's the correct pattern, not a violation; a short non-prose code (errorMessage: 'ERR_001') — a code isn't authorable "content"; and the usual developer- message exemptions (throw new Error(…), console.* args).

Note if you're looking for hardcoded-error-message: that finding type used to live under hardcoded-config (which flagged an errorMessage: literal specifically, regardless of whether it was prose). It moved here — hardcoded-config now owns only the unrelated hardcoded request-url shape — so the whole "message hardcoded in code" concern has one home and one consistent rule (prose-gated, same as every other check in this file).

storage-class

Six checks on setVariable/getVariable/$properties, all rooted in one rule: form-scoped state must ride a data./uistate. namespace so it's unambiguous whether it survives a resume — and a value that can be re-derived should not be persisted at all.

  • Form data in a variable — a setVariable name that matches a dataRef-bound field name. Let the dataRef carry it (binding/importData); don't duplicate it into a variable.
  • Unnamespaced write — a form-scoped setVariable with no data./uistate. prefix. Use data.<name> for fieldless Class-1 data (or the $data accessor directly), uistate.<name> for persisted flow/UI state, or uistate._<name> for ephemeral scratch that must never persist.
  • Write-only variable (warning) — a setVariable whose name is never read anywhere in the analyzed JS. Likely dead, unless the reader is an authored rule or another module the static scan can't see.
  • Unnamespaced read — the read-side mirror of the unnamespaced-write check, for both getVariable(...) and a direct $properties.<key> access.
  • Read inside an init handler — any form-scoped read (namespaced or not) inside a function named *Init. On init/resume, a fragment shouldn't read form state directly at all — the orchestrator should build the custom:*Init event payload from state, and the fragment should consume it via globals.event.payload.
  • Derived value persisted — a form-scoped setVariable whose value is COMPUTED (arithmetic / ternary / template-concat) from a direct form-state read (getVariable(...), .value/.$value, or $properties.<key>). This is a Class-2 projection: re-derive it at the point of use (a shared calc or reactive rule), never store it — a persisted derivation drifts when its inputs change. Exempt: a value cached from an async/fetched result (write inside a .then/.catch/.finally, or an await/request in the value) — that is the irreducible cross-screen cache, not a projection.

Field-scoped access (globals.field, or a chain that descends into a specific named child rather than terminating at the form/fragment-root anchor) is exempt from all five checks — that's a node's own, non-restorable property bag, not journey state.

headless-incompatible-usage

Custom-fn-surface JS (fragment / custom-fn / *-rules.js) shouldn't assume a live browser runtime is present — DOM access, window access, a raw fetch/XMLHttpRequest/axios/$.ajax call instead of the form's request() tool, redirecting the browser directly (window.location, a hand-built <form> submit), or view/keystroke DOM (addEventListener, querySelector/getElementById, createElement except 'form', direct innerHTML/style/classList mutation) that belongs in a component's decorate instead. One rule, one severity key, regardless of which primitive trips it or why it's wrong — each finding still carries its own specific, type-appropriate message.

This rule runs the same underlying checks as custom-function, navigation-in-custom-fn, and should-be-component (each still owns its OTHER, non-browser-runtime findings — see below) — so the same code can trip more than one of the checks this rule wraps, and report more than once (e.g. a document.querySelector inside a custom function is BOTH generic DOM access and, more specifically, view DOM that belongs in a component — you'll see both).

should-be-component

Beyond the view/keystroke DOM findings folded into headless-incompatible-usage, this rule also warns (not errors) on fragment-value-normalization: a field's own value being string-normalized (.toUpperCase(), .trim(), .replace(), …) in orchestration code and written back as the submitted value. Input sanitization like this is a keystroke concern owned by the field's component, or expressible declaratively as the field's native pattern/format constraint — not orchestration code. It's a warning because a deliberate one-off submit-time normalization is legitimate and can't be ruled out statically. (This is distinct from display-format-in-code, which governs presentation, not the submitted data.)

request-error-handling

A request/api-client call has two genuinely distinct failure shapes, and each needs its own handling:

  • An HTTP-level failure (non-2xx response) OR a network/connectivity failure (offline, DNS, CORS, connection refused) both RESOLVE { ok: false, status, body } — neither rejects. A chain that handles failure only in .catch(...) never sees either; branch on if (!res.ok) inside .then instead.
  • A rejecting/malformed request payload — a Promise-valued body that itself rejects, or a synchronous exception while encoding it — genuinely rejects. An .ok check alone never sees this; a real .catch() (not a trivial fail-open one) is still needed.

Two findings, both anchored on every literal request(...)/globals.functions.request(...) call and resolved by climbing the real caller graph (through wrapper functions, await, and Promise.all) — not just the immediate chain — before concluding a gap is genuine:

  • request-catch-only-no-ok-check — a chain has .catch(...) but nothing reads .ok/.status anywhere in its .then callbacks.
  • request-missing-error-handling — a chain (or an awaited call) has neither a .catch(...) nor an .ok/.status read, and none turns up anywhere reachable through its actual callers either.

Not flagged (a documented gap, not a bug): a chain with an .ok check but still no .catch() — reasoning about "has some handling, still missing a piece" needs to know what already exists, which risks a much higher false-positive rate against chains whose rejection is legitimately handled further up an outer chain. Left to human review.

Before / after — the patterns that matter most

async-value-race — read the value where it's ready, not before:

// Wrong: the read runs before the fetch resolves — `stampDuty` is still the old value
const sd = getVariable('stampDuty');
request({ url: '/stampDuty' }).then((res) => setVariable('stampDuty', res.body.amount));
useIt(sd);

// Right: use the value inside the .then, once it's populated
request({ url: '/stampDuty' }).then((res) => {
  setVariable('stampDuty', res.body.amount);
  useIt(res.body.amount);
});

request-error-handling — an HTTP/network failure resolves { ok: false } (a .catch never sees it); a rejecting/malformed request payload genuinely rejects (an .ok check never sees THAT) — both are needed:

// Wrong: no .ok check — a resolved {ok:false} flows on as success with res.body === undefined; no
// .catch either — a genuinely rejecting request produces an unhandled rejection
request({ url: '/x' }).then((res) => setProperty(field, { value: res.body.amount }));

// Right: branch on res.ok AND catch the reject case
request({ url: '/x' }).then((res) => {
  if (!res.ok) { showError(); return; }
  setProperty(field, { value: res.body.amount });
}).catch(showError);

interactive-change-guard — only act on a real user edit, not a data restore:

// Wrong: on resume, restored data re-fires this and yanks focus around
if (field.$value.length === MAX) dispatchEvent(nextField, 'focus');

// Right: a real user change carries `eventSource`; a restore does not
const isUserEdit = !!(globals.event?.payload && 'eventSource' in globals.event.payload);
if (field.$value.length === MAX && isUserEdit) dispatchEvent(nextField, 'focus');

fragment-globals-scope / fragment-qualified-name — a fragment addresses its own scope:

// Wrong: reaches into the parent form — breaks when this fragment is embedded elsewhere
const v = globals.form.employmentPanel.employer.$value;

// Right: portable — resolves wherever the fragment is embedded
const v = globals.fragment.employmentPanel.employer.$value;

field-writes-sibling — broadcast, don't reach across fields:

// Wrong: the employer field directly fills the category field
setProperty(globals.form.employerCategory, { value: category });

// Right: broadcast; the category field fills itself from the event
dispatchEvent(globals.form, 'custom:employerSelected', { category }, true);

rule-vs-code — derived values belong in a rule, not a change handler:

// Wrong: computing a derived value in JS — won't re-run when inputs change, and re-fires on restore
setProperty(globals.field, { value: principal + interest });

// Right: author it as the field's value rule (a form expression), which re-evaluates automatically.

Rules that read your form JSON

storage-class, fragment-qualified-name, fragment-path-validator, and orphan-fragment-handler cross-check against your form/fragment JSON — point them at the folder holding it via the formJsonRoot option (see the config above). component-model finds its component's JSON automatically. When that JSON isn't available, these rules skip only the JSON-dependent check and still run their code-only checks.

Bot-only rules (not in this plugin)

A few analyzers anchor their finding to a JSON node, not a JS line, so they can't be ESLint rules and run in the bot (lint CLI) only:

  • fragment-rule-form-ref — a fragment rule/event expression referencing the absolute form root $form.
  • rules-in-content — rule/expression grammar (events/fd:events/rules/fd:rules, displayValueExpression/validationExpression) authored inline in a form/fragment content JSON; the content layer must carry no rules — move logic to the code/rule-store layer. The framework plumbing custom:setProperty: ["$event.payload"] is skipped; structural fd:* config is never flagged.

Compatibility

  • ESLint >= 8.57 (flat config); ESLint 9 supported.
  • Node 20+.
  • Self-contained — no other dependencies to install.