Skip to content

Releases: cloudfour/eslint-config

v29.0.0

Choose a tag to compare

@spaceninja spaceninja released this 30 Sep 00:06

Major Changes

  • eslint-config-xo is now v4, which brings eslint-plugin-unicorn 76 and
    eslint-package-json 1.0.
    Ten rules arrive at error severity:

    • unicorn/no-async-iterator-callback
    • unicorn/no-unused-builtin-method-return, which replaces the now-deprecated
      unicorn/no-unused-array-method-return and covers more built-ins. A config
      that turns the old name off still loads, but should switch to the new one.
    • unicorn/no-unused-iterator-helper
    • unicorn/no-useless-set-construction
    • unicorn/no-using-resource-escape
    • unicorn/prefer-combined-guards
    • unicorn/prefer-iterator-zip
    • unicorn/prefer-temporal-conversion
    • package-json/no-absolute-paths-in-scripts
    • package-json/no-incompatible-peer-dependency-ranges
    • package-json/require-bin-executable

    Several rules we already had also report more than before.
    unicorn/prefer-ternary, prefer-early-return and prefer-continue now
    recognise more code shapes. The last two also gain a checkShortBodies
    option, and no-break-in-nested-loop and no-immediate-mutation gain options
    of their own, but every new option defaults to off, so the extra reports come
    from the broader matching, not the options.

    Measured across 855 files in three consumers, this is 21 new reports, 13 of
    them auto-fixable
    . Ten are prefer-ternary, all fixable; ten more are
    prefer-early-return and prefer-continue, of which only two are fixable; the
    last is one fixable prefer-combined-guards. None of the other new rules
    reported anything, apart from one require-bin-executable report that came
    only from a bin pointing into an unbuilt dist/.

  • The supported TypeScript range is now declared: >=6.0.0 <6.1.0. It is an
    optional peer dependency, so JavaScript-only projects are unaffected. The
    range was always there, set by what typescript-eslint and eslint-config-xo
    support, but nothing said so, and projects on other versions found out the
    hard way. On TypeScript 5.x, npm quietly installed a second, unused
    typescript@7 inside this package. On TypeScript 7, install printed a stream of
    peer warnings, and linting failed later.

    Now, installing or upgrading this package in a project whose TypeScript is
    outside the range fails immediately with an ERESOLVE error that names
    @cloudfour/eslint-config and the range it accepts. To upgrade, move the
    project to TypeScript 6.0 first. We will widen the range in the same release
    that typescript-eslint supports a newer TypeScript.

    This does not stop a project already on this version from bumping TypeScript
    itself. npm treats a change to the project's own typescript as deliberate
    and only warns, so a Renovate PR offering TypeScript 7 still opens and fails at
    lint or build, as it does today.

Minor Changes

  • Lockfiles are no longer linted. xo now ignores **/package-lock.json and
    **/npm-shrinkwrap.json by default, so our lockfile layer, which only turned
    off json/no-empty-keys for npm's root-package key, had nothing left to apply
    to and has been removed.

  • @typescript-eslint/no-floating-promises allows un-awaited node:test
    calls.
    test(), it(), describe() and their siblings return promises
    that the runner tracks itself, and xo now lists them as known-safe.

v28.0.0

Choose a tag to compare

@spaceninja spaceninja released this 17 Sep 20:23

Major Changes

  • @typescript-eslint/no-floating-promises and
    @typescript-eslint/restrict-template-expressions are now on.
    Both had been
    off since 2020, when our TypeScript rules were first added. eslint-config-xo
    enables both, so this change deletes our two overrides rather than configuring
    anything: the rules arrive with xo's options, not the plugin defaults.

    Measured across 296 TypeScript files in three consumers — c4-sw-ac-patterns
    (221), linkedin-talent-connect (66) and lighthouse-parade (9) — this is
    nine no-floating-promises reports and two
    restrict-template-expressions
    . Neither rule is auto-fixable, though
    no-floating-promises offers editor suggestions, so anything either one
    catches has to be edited by hand. That is what makes this a major release.

    no-floating-promises reports a promise that is started and then abandoned —
    no await, no .catch(), no void. It runs with xo's ignoreVoid, so
    void doThing() marks one as deliberately unawaited, and xo relaxes no-void
    to permit that form. The old reasoning for turning it off — that humans should
    decide when to handle a rejection — is what void expresses; the difference is
    that the decision is now visible at the call site instead of indistinguishable
    from an oversight. Six of the nine reports in the sample were un-awaited
    waitFor() calls in test files, where the assertion inside could not fail the
    test.

    restrict-template-expressions runs with xo's allowNumber: true and leaves
    allowAny at its default, so interpolating an any into a template literal is
    still allowed — the specific allowance our override was written for survives
    the rule being on. What it reports is values with no useful string form. Both
    reports in the sample were never, from setters whose type guard narrows the
    guard's own failure branch.

    The @typescript-eslint/no-unsafe-* family stays off, and so does
    no-unsafe-type-assertion, which is a much larger migration (377 reports in
    the same sample) and a separate question.

  • func-names is now on, at xo's setting of never. It reports a named
    function expression — const handler = function onClick() {} — and wants
    the name dropped, because since ES6 the variable name is inferred anyway. It
    has nothing to say about function declarations.

    We had turned it off in the original 2018 config with no reason recorded
    anywhere, so this adopts xo's position rather than reversing a decision of
    ours. Zero reports across 434 files in three consumers — modern code reaches
    for arrow functions, which the rule does not touch.

    The case it does cost you is naming a callback purely for debugging, such as
    setTimeout(function tick() {}, 100). Inference does not apply to a function
    passed straight as an argument, so that name is the only thing that shows up
    in a stack trace or profiler. If you want it, // eslint-disable-next-line func-names on that call is the escape hatch.

  • capitalized-comments now extends xo's ignore pattern instead of replacing
    it.
    The pattern exists so autofix does not capitalise commented-out code,
    which then has to be un-capitalised when you uncomment it. Ours was forked
    from xo's years ago and had drifted: we were missing the starts xo added since
    (if (, for (, while (, switch (, class , import , export ,
    type-coverage:), and xo was missing three of ours (return, await,
    console). Both sets now apply, and anything xo adds later arrives on its own.

    Almost entirely a widening, but not purely: xo matches const with a
    trailing space where our old pattern matched a bare const, so a prose comment
    that merely starts with those letters — // constant — is now reported where
    it was not. One such report across 434 files.

Minor Changes

  • @typescript-eslint/strict-boolean-expressions is now off. It arrived in
    v26.0.0 with the move to eslint-config-xo, which enables it with
    allowString and allowNumber tightened to false. It was the single largest
    source of TypeScript-only reports in the config, and measurement did not
    justify the cost: 165 reports across 294 TypeScript files in three
    consumers, with no genuine defect in a 34-report sample spanning every
    category the rule distinguishes. What it flagged was optional string
    attributes, || '' defaults and guard clauses — cases where '', 0 and
    absent legitimately mean the same thing.

    Loosening it rather than removing it does not work, which is worth recording
    because it is the obvious first idea. Setting allowString/allowNumber back
    to the plugin defaults clears only 21% of the reports, because three quarters
    of the remainder is string | undefined in a condition — rejected by xo and by
    the rule's own defaults alike. Allowing enough to clear that leaves eight
    reports, and none of those is a defect either.

    Upstream has declined to loosen the rule
    (xojs/eslint-config-xo#108),
    so this is ours to set rather than something to wait on. Projects that want it
    can turn it back on in their own config.

    No project gains a report. Anyone who has already rewritten conditions to
    satisfy it keeps working code; the rule simply stops asking.

  • Twenty entries that restated a value eslint-config-xo already sets have
    been removed.
    No rule changes severity or options as a result — every one was
    verified to resolve identically with and without our line, and the rule
    inventory is unchanged across the removal.

    What it changes is the future. A rule we no longer name is one we would follow
    xo on if it changed its position, rather than silently pinning the old value.
    That is deliberate: restating a value is a standing commitment to track every
    option that rule later grows, and two of our entries had already drifted out of
    step that way without anyone noticing.

v27.1.0

Choose a tag to compare

@spaceninja spaceninja released this 17 Sep 20:18

Minor Changes

  • @typescript-eslint/no-restricted-types is now scoped to TypeScript files.
    We had it configured in the layer that covers every code file, which quietly
    made the @typescript-eslint plugin mandatory for linting anything at all. A
    project with no TypeScript installed did not skip the rule, it failed outright:

    A configuration object specifies rule "@typescript-eslint/no-restricted-types",
    but could not find plugin "@typescript-eslint".
    

    No project gains or loses a report. The rule only ever matches type
    annotations, so it could not fire on JavaScript in the first place — measured
    against a real consumer project it changed nothing across 66 TypeScript files
    and 120 single-file components, and the rule inventory records exactly one
    difference, on the JavaScript probe. What changes is that a JavaScript-only
    project can now lint with typescript absent from node_modules.

    Note that npm may install TypeScript anyway. eslint-config-xo depends on
    typescript-eslint, which declares a required typescript peer, so a
    project with no TypeScript still gets roughly 31 MB of it. Nothing in this
    config can prevent that; it needs a fix upstream, which we have reported as
    xojs/eslint-config-xo#126.

v27.0.0

Choose a tag to compare

@spaceninja spaceninja released this 15 Sep 18:02

Major Changes

  • Update eslint-config-xo to v2, which moves eslint-plugin-unicorn from
    v73 to v74. The bump adds, removes and reconfigures no unicorn rule — v74's
    changes are fixes to rules we already run. Measured against a real consumer
    project, it produced three fewer reports across 64 TypeScript files, where
    unicorn/prefer-number-coercion and unicorn/prefer-string-replace-all had
    been flagging cases they misread.

  • n/prefer-process-get-builtin-module is now on, added by xo v2. It reports
    require('node:fs') and await import('node:fs') for built-in modules and
    wants process.getBuiltinModule('node:fs') instead. It is not auto-fixable,
    so whatever it catches has to be edited by hand. This is the only rule the
    upgrade turns on, and the only source of new errors in it.

    Most projects will never see it. The rule switches itself off unless the
    nearest package.json declares an engines.node falling entirely within
    ^20.16.0 || >=22.3.0 — the versions where process.getBuiltinModule is
    guaranteed to exist. A package with no engines.node at all, or one as loose
    as >=20, is left alone.

    Where it does apply, the payoff is in ES modules, where it removes the need
    for createRequire. It fires in CommonJS too, where require is the native
    idiom and the rewrite buys nothing, so build scripts and .cjs files are the
    likeliest place to meet it. Turn it off for those paths in your own config if
    it is noisy:

    {
    	files: ['**/*.cjs'],
    	rules: { 'n/prefer-process-get-builtin-module': 'off' },
    }

v26.1.0

Choose a tag to compare

@spaceninja spaceninja released this 31 Aug 21:45

Follow-up to v26, from rolling it out across our repos. Everything here either
turns a rule off or widens what one accepts, so no project should gain new
errors from upgrading.

Minor Changes

  • Scope xo's package.json rules to the root manifest. They were applied to
    every **/package.json, but they describe a package's manifest. Packages that
    ship a dual CommonJS/ESM build also ship marker files whose entire content is
    {"type": "commonjs"} or {"type": "module"}, and those collected demands for
    name, version, license, keywords and an entry point they will never
    have — 13 reports across two two-line files in one of our packages. Worse,
    prefer-type-module told a {"type": "commonjs"} marker to become
    "module", and following that breaks the CommonJS half of the package.

    If you keep real manifests outside the repository root, such as npm
    workspaces, widen the scope in your own config. See the README.

  • Turn off package-json/prefer-exports, package-json/prefer-type-module
    and package-json/require-engines.
    Adding exports or "type": "module"
    changes what a published package exposes to people already consuming it: deep
    imports start throwing ERR_PACKAGE_PATH_NOT_EXPORTED, and require() stops
    working. Both are worth doing, but as deliberate major releases rather than as
    lint errors, and both are meaningless for applications that never publish.
    engines.node is fair for packages that run in Node, but we publish browser
    libraries with no Node runtime to describe, and inventing a range to satisfy
    the rule makes the field less trustworthy where it matters.

    package-json/prefer-files-field stays on. It skips private packages already,
    so it only speaks up for genuinely publishable ones — and it caught a real bug
    for us, where a published tarball contained the entire repository.

    The README now lists these three as recommended settings to adopt in packages
    where they apply.

  • Turn off @html-eslint/require-open-graph-protocol and
    @html-eslint/require-meta-description.
    OGP requires og:title, og:type,
    og:url and og:image on every HTML file in a project. Demo pages, test
    fixtures, component examples and email templates all match, and none of them
    are ever shared as a link. Which pages get Open Graph tags, and what those tags
    say, is a per-page content decision. Neither rule is in html-eslint's own
    recommended set; xo adds them. The rest of xo's HTML rules are kept, including
    require-lang, require-meta-charset and require-meta-viewport.

  • Turn off unicorn/no-non-function-verb-prefix. It requires anything named
    with a verb prefix to be a function, so a variable holding a callback gets
    reported as `setFocus` starts with `set`, so it should be a function.
    Same family of naming opinion as unicorn/consistent-boolean-name and
    unicorn/name-replacements, which we already disable for the same reason.

  • Allow indented continuation lines in JSDoc blocks. jsdoc/check-indentation
    and jsdoc/check-line-alignment between them forbade indenting the wrapped
    lines of a @param description, and --fix silently flattened existing
    indentation:

      * @param {string} opts.waitMode - Determine how the library should check that
    - *   hiding transitions are complete. The options are `'transitionEnd'`,
    + * hiding transitions are complete. The options are `'transitionEnd'`,

    A tag's description runs until the next tag and leading whitespace is stripped
    before parsing, so the indentation is purely cosmetic — it just makes it easier
    to see where one @param ends and the next begins. Both styles are now
    accepted. An indented tag line is still reported, since that is a real
    mistake.

Notes for anyone upgrading from v25

These describe v26 behaviour rather than anything new in this release, but they
are the two things that caught us out, so they are worth writing down.

  • @html-eslint/require-button-type autofixes to type="submit". The rule
    is a good one and stays on, but its fixer inserts submit unconditionally,
    with no regard for whether the button is in a form. submit is the HTML
    default, so nothing changes behaviourally and nothing looks wrong in review —
    but for a JavaScript-driven button the markup then says the opposite of what
    is true. Check the result on any page with buttons:

    - <button class="js-toggle">Show</button>
    + <button type="button" class="js-toggle">Show</button>
  • Rule overrides need a files glob. xo scopes its layers to the file types
    they lint and registers plugins on those layers, so a rules block with no
    files now also applies to JSON, Markdown, CSS and HTML, where those plugins
    were never registered. ESLint then refuses to load the config at all:

    A configuration object specifies rule "jsdoc/check-indentation",
    but could not find plugin "jsdoc".
    
      {
    +   files: ['**/*.{js,cjs,mjs,ts}'],
        rules: {
          'jsdoc/check-indentation': 'off',
        },
      }

    This is easy to misread as a dependency problem, because it passes when you
    lint a single file and only fails on eslint ..

v26.0.0

Choose a tag to compare

@spaceninja spaceninja released this 27 Aug 23:40

Breaking Changes

  • Rebuild on eslint-config-xo v1 and remove the vendored copy of eslint-config-standard. xo now bundles most of what we assembled by hand, so eslint-config-prettier, eslint-plugin-import-x, eslint-plugin-jsdoc, eslint-plugin-n, eslint-plugin-unicorn and typescript-eslint are no longer direct dependencies — they arrive through xo, and xo decides their versions. Three direct dependencies remain: eslint-config-xo, eslint-plugin-promise and globals. Our own overrides are unchanged.

    Of the 106 rules the vendored standard config still contributed, xo already enabled 95. The seven that survive (no-empty-character-class, no-invalid-regexp, no-useless-backreference, n/handle-callback-err, n/no-callback-literal, n/no-exports-assign, promise/param-names) are now set explicitly. standard's remaining value was formatting, which Prettier already handles for us.

    This turns on a meaningful number of new rules. Measured against a real project, roughly 250 new reports across 64 TypeScript files, mostly from require-unicode-regexp (the v flag), @typescript-eslint/strict-boolean-expressions, curly and n/prefer-global/process.

    xo also lints package.json, JSON, Markdown and CSS, which it did not before. One of its package.json rules is off: dependency-version-range, because we pin exact versions deliberately and it fires on every project, including applications that never publish anything. The rest apply, including sort-properties, which reorders top-level fields to npm's canonical sequence the first time it runs.

    Three of xo's opinions are turned back off because they contradict decisions we had already made:

    • @typescript-eslint/no-restricted-types no longer bans null as a type. That is the same opinion as unicorn/no-null, which we have always disabled. xo's other restrictions are kept.
    • @typescript-eslint/naming-convention is off. It cannot distinguish our names from names that come from someone else's contract, so it flags things like Authorization headers and generated GraphQL types.
    • jsdoc/require-asterisk-prefix is back to always. xo sets it to never, which wants JSDoc written without the leading asterisk on continuation lines.
  • Import rules move from the import/* namespace to import-x/*. We used to depend on eslint-plugin-import and later replaced it with eslint-plugin-import-x, registered under the name import to keep rule names stable. eslint-plugin-import has not released since June 2025 and does not support ESLint 10, which is what forced the swap. Now that xo bundles import-x under its own import-x namespace, we use that directly. Any eslint-disable comment naming an import rule needs updating:

    - // eslint-disable-next-line import/order
    + // eslint-disable-next-line import-x/order
  • Replace main with exports. The package now declares "exports": "./eslint.config.js" instead of "main", and adds "sideEffects": false. import cloudFourConfig from '@cloudfour/eslint-config' is unaffected. What changes is that deep imports are now blocked: import '@cloudfour/eslint-config/eslint.config.js' throws ERR_PACKAGE_PATH_NOT_EXPORTED. That path resolved to the same module as the package root, so it was an unusual thing to write, but anything doing it needs to import the package root instead.

  • Drop ESLint 9 support. The eslint peer range is now ^10.6.0. The previous >= 9 range was never accurate either — eslint-plugin-unicorn has required >=9.38.0 since v63.

  • Remove the custom @cloudfour/prefer-early-return rule, along with the @cloudfour plugin namespace. eslint-plugin-unicorn ships an equivalent unicorn/prefer-early-return, which we now use with maximumStatements: 2 so it reports exactly the same cases the old rule did. We would rather not maintain a lint rule that exists upstream.

    Any eslint-disable comments naming the old rule will now fail with "Definition for rule '@cloudfour/prefer-early-return' was not found". Replace them:

    - // eslint-disable-next-line @cloudfour/prefer-early-return
    + // eslint-disable-next-line unicorn/prefer-early-return
  • Change the supported Node versions from >=22.16.0 to ^22.13.0 || >=24, matching what ESLint and our other dependencies actually support. This drops Node 23.x, which was never really supported.

Major Changes

  • Require ESLint v10.6 or newer. eslint-config-xo declares >=10.4, but it configures no-constant-binary-expression with an option ESLint did not support until v10.6, so its own range is optimistic. Our CI job that runs the suite against the oldest ESLint we advertise is what caught this.

  • ecmaVersion is no longer pinned to 2022; xo's latest applies. Pinning it lower made require-unicode-regexp unsatisfiable, since the v flag it requires is newer than the syntax we allowed.

  • eslint-plugin-unicorn moves from v63 to v73 (via xo), which enables around 178 additional rules. Some existing eslint-disable comments for unicorn rules may become stale and need removing.

    Renamed upstream, so any eslint-disable comments using the old names need updating:

    • unicorn/prevent-abbreviations → unicorn/name-replacements (still off)
    • unicorn/no-array-for-each → unicorn/no-for-each
    • unicorn/prefer-dom-node-dataset → unicorn/dom-node-dataset
    • unicorn/no-hex-escape was removed, replaced by unicorn/prefer-unicode-code-point-escapes

    Newly disabled, because they conflict with conventions or duplicate a rule we already run:

    • unicorn/consistent-boolean-name — requires booleans to be named isFoo/hasFoo/etc. A naming opinion, left to reviewers, consistent with our stance on unicorn/name-replacements.
    • unicorn/single-line-block-comment-style — would expand single-line /** @type {…} */ JSDoc across multiple lines.
    • unicorn/require-array-sort-compare — can't distinguish string arrays from number arrays without type information. Replaced with the type-aware @typescript-eslint/require-array-sort-compare for TypeScript files.

    Reconfigured:

    • unicorn/prefer-early-return replaces our removed custom rule, set to maximumStatements: 2 to match it. See the breaking change above.
    • unicorn/filename-case now checks directory names as well as filenames. We ignore __tests__-style directories so conventional names like __tests__, __mocks__ and __snapshots__ don't have to be renamed.
    • @typescript-eslint/no-unnecessary-boolean-literal-compare is now off in favour of unicorn/no-unnecessary-boolean-comparison, which covers the same cases without requiring strictNullChecks and also applies to JavaScript.
  • eslint-plugin-n moves from v17 to v18 (via xo).

  • eslint-plugin-jsdoc moves from v62 to v63 (via xo). An earlier commit in this release took it to v64 directly; xo pins ^63.3.3, so v63 is what ships.

Minor Changes

  • Only publish eslint.config.js to npm. The package previously also shipped repository files such as .editorconfig, .nvmrc, .renovaterc.json and the CI workflow.

v25.0.1

Choose a tag to compare

@spaceninja spaceninja released this 12 Feb 23:01

Minor Changes

  • Disable a few stylistic rules that aren't currently covered by eslint-cofig-prettier.

v25.0.0

Choose a tag to compare

@spaceninja spaceninja released this 12 Feb 22:42

Breaking Changes

  • Update minimum Node version to v22

Major Changes

  • Update eslint-config-prettier to v10
  • Update eslint-plugin-jsdoc to v62
  • Update eslint-plugin-unicorn to v63

v24.0.0

Choose a tag to compare

@spaceninja spaceninja released this 14 Nov 17:36

Breaking Changes

  • Changed from a "super plugin" to a flat config file
  • Changed package name from @cloudfour/eslint-plugin to @cloudfour/eslint-config

Major Changes

  • Update eslint-plugin-jsdoc from v46 to v50
  • Update eslint-plugin-n from v16 to v17
  • Update eslint-plugin-promise from v6 to v7
  • Update eslint-plugin-unicorn from v48 to v56
  • Update typescript-eslint from v6 to v8

v23.0.0

Choose a tag to compare

@github-actions github-actions released this 22 Aug 18:21
ab31b70

Major Changes

Minor Changes

  • #551 f11e794 Thanks @calebeby! - Re-export disable-type-checked config from @typescript-eslint/eslint-plugin