Skip to content

Upgrade the lint and format toolchain: Prettier 3, ESLint 9, Stylelint 16 - #2416

Merged
spaceninja merged 6 commits into
mainfrom
lint-and-format
Aug 21, 2026
Merged

Upgrade the lint and format toolchain: Prettier 3, ESLint 9, Stylelint 16#2416
spaceninja merged 6 commits into
mainfrom
lint-and-format

Conversation

@spaceninja

Copy link
Copy Markdown
Member

Overview

This is item 4 of the dependency modernization sequence: Prettier 3, the ESLint
toolchain, and Stylelint 16. It has knock-on value in two directions. Prettier 3 is what
lets @whitespace/storybook-addon-html come back, since v9 supports Storybook 10 but
peers on prettier ^3.7.1 to pretty-print the markup it displays. And moving off
@cloudfour/eslint-plugin onto its successor @cloudfour/eslint-config lifts the ESLint
ceiling that the plan had listed as blocking, which in turn is what gated moving past
TypeScript 5.

The plugin peers on eslint: ^8.0.0 even at its latest release. The config peers on
>= 9 and bundles typescript-eslint 8, so ESLint moves 8.33 → 9.39.5 and the plugins it
wraps jump several majors (unicorn 47 → 63, jsdoc 46 → 62, n 17). That also means flat
config: .eslintrc.js and .eslintignore become eslint.config.mjs, and rule names lose
the @cloudfour/ prefix now that plugins register under their own names. We stop at
ESLint 9 rather than 10
because eslint-plugin-react — which supplies the
jsx-uses-vars rule the MDX files rely on — peers at ^9.7 with no 10 support.

Stylelint stops at 16, not 17 as the plan assumed: stylelint-config-cloudfour 10 and
stylelint-use-logical-spec both cap there independently, so 17 would mean running our
own shared config unsupported. stylelint-config-prettier is deleted rather than
upgraded — stylelint 15 deprecated its stylistic rules and 16 removed them, so it has
nothing left to turn off. That retirement did not extend to stylelint-scss, which still
ships and enables every rule the old config listed by hand, so those disables stay behind
with a comment explaining why they outlived the package that motivated them.

The commits are ordered so the mechanical churn is separable: the Prettier version bump,
then the repo-wide reformat it causes, then each linter.

Prettier 3 does not fix the MDX corruption, so *.mdx stays ignored

The note in .prettierignore blamed the corruption on Prettier 2 and said the rule could
go once we were on 3. That turned out to be wrong, and it seemed worth recording properly
rather than quietly re-adding the line. Prettier 3 has no MDX 3 support either
(prettier/prettier#12209) and still mangles multi-line {/* … */} comments into
{/_ … _/} (prettier/prettier#15163, open since 2023) — output that is not valid MDX, so
Storybook fails to index the file.

There is no way to opt out for just those files. {/* prettier-ignore */} has no effect,
and <!-- prettier-ignore --> does stop Prettier but is not valid MDX 3 — the only syntax
Prettier honours is the one MDX rejects. Since only multi-line comments are affected,
the other 80 files were formatted once by hand and the ignore rule reinstated with the
real reason recorded.

The ESLint upgrade fixed a config bug rather than causing one

eslint-plugin-mdx v1 set eslint-mdx as the parser for every file, not just Markdown.
That is the cause of the "eslint isn't merging the overrides as expected" comment this PR
removes, and of unicorn/no-empty-file reporting four token files whose only statement is
module.exports. With v3 scoping its parser correctly, .ts files pick up
@typescript-eslint/parser from the shared config on their own, so the workaround is gone.

Three config decisions in eslint.config.mjs are worth a reviewer's attention:

  • settings.n.version is pinned to our development Node. Without it the
    n/no-unsupported-features/* rules fall back to engines.node, which describes what
    consumers of the published package need — so our gulpfile, Storybook config and tests
    were being checked against Node 12. Setting the lint target here deliberately avoids
    changing that consumer-facing field as a side effect; Dependency modernization: Node 24, Vite, and Storybook 10 #2391 already notes it deserves
    its own decision.
  • Plain Markdown now reaches the JS rules, where rules written for code misfire on
    prose. no-irregular-whitespace flags the non-breaking spaces indenting the directory
    tree in CONTRIBUTING.md, and filename-case wants README.md renamed. Both are off for
    *.md, which mdx/remark lints instead.
  • unicorn/prefer-global-this is off for src. Its auto-fix rewrites
    window.setTimeout to globalThis.setTimeout, which resolves to Node's overload and
    returns a Timeout rather than the number these components store — a type error that
    lint reported clean and only tsc caught. In browser-only code window is both correct
    and better typed.

Published output

Everything here is tooling except one line. The Prettier reformat and the ESLint fixes
were both verified to leave dist byte-for-byte identical — cloudfour-patterns.mjs and
.min.js are unchanged, and the only differences in the unminified UMD bundle are comment
text. Stylelint surfaced three genuine findings, of which only the place-self collapse
changes published CSS, and only into an equivalent declaration; that one carries a patch
changeset.

storybook-mobile, parked alongside the HTML addon during the Storybook migration, stays
dropped: it is still at 1.0.1 and peers on React ^17 while Storybook 10 is on 19.

Screenshots

Testing

Run nvm use first — this needs Node 24.19.0.

  • Run npm ci, then npm start, and wait for Storybook to open.
  • Open Components → Button → Button Element. The button should render normally.
  • In the addons panel at the bottom, there should now be an HTML tab alongside
    Controls, Actions, Interactions, Code and Accessibility. Select it — it should show
    the story's markup, indented and syntax-highlighted, with a Copy button.
  • Open Design → Icons. All icons should render, none blank, and they should be
    listed in alphabetical order.
  • Open Design → Illustrations. Same — everything renders, in alphabetical order.
  • Open Design → Modular Scale. The table should list steps counting up from -6,
    with real sizes rather than 0em.
  • Open Components → Sky Nav → Default, then switch to another story and back. The
    nav should render closed with a working toggle button, not stuck open. At a narrow
    viewport, the toggle should open and close it with animation.
  • Open Components → Subscribe → Default, click into the form, then click away.
    The form should hide again after a short delay.
  • Open Objects → Container and Objects → Button Group. Both docs pages should
    render their prose and demos, with no stray {/_ … _/} text anywhere on the page.
  • Open Components → Ground Nav → Default and widen the browser past roughly
    1200px. The social icons should sit at the top-right of their area, unchanged from
    before this PR.
  • Spot-check a few component docs pages. Twig source previews should still appear with
    syntax highlighting.

Prettier 3 changes the `trailingComma` default from `es5` to `all`, and
breaks multi-value CSS declarations onto their own lines. Mechanical --
`dist` is byte-for-byte identical before and after.
Prettier 3 does not fix the MDX corruption `.prettierignore` blamed on
Prettier 2 -- it has no MDX 3 support either (prettier/prettier#12209),
and mangles multi-line `{/* ... */}` comments into invalid `{/_ ... _/}`
(prettier/prettier#15163). There is no per-file opt out: `prettier-ignore`
has no effect, and the HTML-comment form Prettier honours is not valid MDX.
Only multi-line comments are affected, so the other 80 files were formatted
by hand and the rule reinstated with the real reason recorded.
Replaces `@cloudfour/eslint-plugin`, which is superseded by
`@cloudfour/eslint-config`, and moves to flat config: `.eslintrc.js` and
`.eslintignore` become `eslint.config.mjs`.
This lifts the ceiling the plugin imposed. The plugin peers on `eslint: ^8.0.0`
even at its latest release; the config peers on `>= 9` and bundles
typescript-eslint 8, so ESLint moves 8.33 -> 9.39.5 and the plugins it wraps
jump several majors (unicorn 47 -> 63, jsdoc 46 -> 62, n 17). We stop at
ESLint 9 rather than 10 because eslint-plugin-react, which supplies the
`jsx-uses-vars` rule the MDX files need, peers at `^9.7` and has no 10 support.
Rule names lose the `@cloudfour/` prefix, since the config registers plugins
under their own names -- `@cloudfour/n/...` becomes `n/...`, and
`@cloudfour/typescript-eslint/...` becomes `@typescript-eslint/...`. Only two
eslint-disable comments referenced the old names.
`eslint-plugin-mdx` moves 1 -> 3 in the same change, because it is the same
knot. v1 set `eslint-mdx` as the parser for *every* file rather than just
Markdown, which is what the "eslint isn't merging the overrides as expected"
comment in the old config was working around, and what made
`unicorn/no-empty-file` report four token files whose only statement is
`module.exports`. With v3 scoping its parser correctly, `.ts` files pick up
`@typescript-eslint/parser` from the shared config on their own, so that
override is gone.
Three config decisions worth recording:
- `settings.n.version` is pinned to our development Node. Without it the
`n/no-unsupported-features/*` rules fall back to `engines.node`, which
describes what consumers of the published package need -- so our gulpfile,
Storybook config and tests were being checked against Node 12. Setting the
lint target here avoids changing a consumer-facing field as a side effect.
- Plain Markdown now reaches the JS rules, where rules written for code misfire
on prose. `no-irregular-whitespace` flags the non-breaking spaces indenting
the directory tree in CONTRIBUTING.md and `filename-case` wants README.md
renamed, so both are off for `*.md`, which `mdx/remark` lints instead.
- `unicorn/prefer-global-this` is off for `src`. Its auto-fix rewrites
`window.setTimeout` to `globalThis.setTimeout`, which resolves to Node's
overload and returns a `Timeout` rather than the `number` these components
store -- a type error that only `tsc` caught. In browser-only code `window`
is both correct and better typed.
The rest is the new rules' findings: JSDoc types where they were missing or
`any`, `replaceAll` over global-regex `replace`, spread over `concat`,
`path.join` over string concatenation, named default exports, and import order.
stylelint 14 -> 16.26.1, stylelint-config-cloudfour 6 -> 10, stylelint-scss
4 -> 7, stylelint-use-logical-spec 4 -> 5.
Stylelint stops at 16, not 17 as the plan assumed. Both
stylelint-config-cloudfour 10 and stylelint-use-logical-spec cap there
independently, so 17 would mean running our own shared config unsupported.
`stylelint-config-prettier` is removed rather than upgraded: stylelint 15
deprecated its stylistic rules and 16 removed them, leaving the package with
nothing to turn off, and it peers on `stylelint < 15` anyway. That retirement
did not extend to stylelint-scss, which still ships and enables all of the
rules the old config listed by hand -- three of them fire on Prettier's own
output -- so those disables stay, with the comment rewritten to say why they
outlived the package that motivated them.
Config changes for stylelint 16's new checks:
- `media-feature-name-no-unknown` reads the Sass module namespace in
`@media (width >= breakpoint.$l)` as a feature name. It handles a bare
`$variable` in range syntax but not a namespaced one, and only in the range
form -- `(min-width: breakpoint.$m)` parses fine. Ignored by name.
- `declaration-block-no-redundant-longhand-properties` now reports gap under
its modern name, so the existing `grid-*-gap` exemption no longer matched.
Added the block/inline shorthands too, which utilities/spacing documents
avoiding for browser support.
Three genuine findings fixed at the source: a redundant leading underscore in
a partial name, empty parentheses on an argumentless mixin include, and a
collapsible `place-self` pair. Only the last changes published CSS, and only
to an equivalent declaration, so it carries a patch changeset.
The addon was dropped during the Storybook 10 migration and could not come
back until now: v9 supports Storybook 10 but peers on `prettier ^3.7.1`,
which it uses to pretty-print the markup it displays.
`storybook-mobile`, the other addon parked at the same time, stays dropped.
It is still at 1.0.1 and peers on React ^17 while Storybook 10 is on 19.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2dc1e55

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
NameType
@cloudfour/patternsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlifyBot commented Aug 21, 2026

Copy link
Copy Markdown

Deploy Preview for cloudfour-patterns ready!

NameLink
🔨 Latest commit2dc1e55
🔍 Latest deploy loghttps://app.netlify.com/projects/cloudfour-patterns/deploys/6a88b930b6afaf0008c9e372
😎 Deploy Previewhttps://deploy-preview-2416--cloudfour-patterns.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@spaceninja