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.12

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

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 whose failure is handled only in .catch. AEM requests don't throw on failure — they succeed with { ok: false } — so the .catch never runs and errors slip through as success. Check res.ok instead. | | 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 async custom function (the form silently ignores it, so it never runs), field.parent (undefined here — use field.$parent), or reading _jsonModel directly (bypasses change tracking, so dependent rules won't re-run). | | custom-function | Browser access (window, the DOM) or a raw HTTP call inside a custom function. Custom functions run headless, so this breaks outside the browser. Also flags many single-field updates that should be one bulk update. | | navigation-in-custom-fn | Redirecting the browser from a custom function (window.location, or a hand-built <form> submit). Use the form's own submit/redirect. |

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, or reading form state inside a startup handler. | | 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 without writing it back to the form model, so the model and the screen disagree. | | runtime-cls | Changing styles/classes while the form loads in a way that shifts the layout (a visible jump for the user). |

Auto-fixable: field-parent rewrites .parent.$parent; storage-class-namespace suggests data. / uistate. names.

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 — AEM requests resolve { ok: false }, they don't throw:

// Wrong: .catch never fires; a failed request flows on as success with res.body === undefined
request({ url: '/x' }).then((res) => setProperty(field, { value: res.body.amount })).catch(showError);

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

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.

Compatibility

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