feat!: drop unmaintained eslint-config-airbnb-base#35
Conversation
Replace airbnb-base with @eslint/js recommended + eslint-plugin-import recommended. eslint-config-airbnb-base@15.0.0 has been the latest published version since 2022 and still pins its eslint peer dep to ^7.32.0 || ^8.2.0. Consumers using eslint 9 see ERESOLVE warnings on every install. The internal `overrides` block this package shipped to paper over the peer dep was a no-op because npm only honors `overrides` from the root package, not from transitive deps. The conflict surfaces hard when consumers enable npm's `min-release-age` setting: npm strict-resolves peer deps and the warning escalates to `ENOVERSIONS - No versions available for eslint`.
There was a problem hiding this comment.
Pull request overview
This PR removes the unmaintained eslint-config-airbnb-base (which is incompatible with ESLint 9 peer deps under strict resolution) and switches @apify/eslint-config to rely on native flat-config building blocks (@eslint/js + eslint-plugin-import) while keeping the package installable without peer-dep resolution failures.
Changes:
- Drop
eslint-config-airbnb-base(and the now-useless internaloverrides) to eliminate ESLint 9 peer-dep conflicts. - Remove unused/dead dependencies tied to the legacy config loading path (e.g.,
@eslint/compat,@eslint/eslintrcusage). - Explicitly include
@eslint/jsrecommended +eslint-plugin-importrecommended flat config, and setlanguageOptions.sourceType = 'module'to preserve prior module parsing semantics.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| package.json | Removes eslint-config-airbnb-base, @eslint/compat, and the no-op overrides block to fix install-time peer resolution issues. |
| index.js | Replaces legacy FlatCompat-based AirBnB extension with native flat config entries (@eslint/js + eslint-plugin-import) and restores sourceType: 'module'. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jirimoravcik
left a comment
There was a problem hiding this comment.
Most of the platform repositories do not use any formatter at the moment. Removing the stylistic rules would be a massive breaking change.
|
Well, we can't keep things the way they are now, this was always wrong, you can't use This is a major bump, so you could keep using the old version in private repos. Or just adopt a formatter. We keep postponing this for no reason, all tooling repos already have it, and it was super simple to adopt. Alternatively, we could add formatting rules here via https://eslint.style/, but I would rather go with the formatters. Either way, we need to get rid of that unmaintained dependency. This was causing warnings on install for years now. And if we want to enforce min release age, those warnings become errors, effectively blocking the adoption. |
Brings back the meaningful (non-stylistic) rules that airbnb-base used to provide, declared explicitly in this config so we don't depend on the unmaintained airbnb-base package. Stylistic rules are intentionally left out — Prettier (or any formatter) is the right tool for those. Categorized inline as: - Modern JS / hygiene (eqeqeq, no-var, prefer-const, prefer-template, prefer-arrow-callback, prefer-rest-params, prefer-spread, prefer-object-spread, object-shorthand, no-useless-rename, etc.) - Defensive / bug-catching (no-param-reassign, consistent-return, default-case, default-case-last, default-param-last, radix, no-throw-literal, no-shadow, array-callback-return, no-promise-executor-return, no-template-curly-in-string, no-loop-func, no-constructor-return, no-unused-expressions, prefer-promise-reject-errors, etc.) - Security (no-eval, no-implied-eval, no-new-func, no-script-url, no-proto, no-extend-native, no-caller, no-iterator) - Quality (dot-notation, guard-for-in, no-alert, no-bitwise, no-labels, no-restricted-globals for legacy isFinite/isNaN, new-cap, grouped-accessor-pairs, prefer-regex-literals, etc.) - Imports (import/first, import/no-cycle, import/no-self-import, import/no-mutable-exports, import/no-useless-path-segments, import/no-absolute-path, import/no-dynamic-require, import/newline-after-import) All rule values match airbnb-base's exact configuration where applicable. Rules that airbnb itself had set to 'off', or that have been removed/deprecated in eslint 9, are not included.
|
Discussed this with @fnesveda on slack, I've added some of the missing rules back, keeping the stylistic ones off. PR description is updated in this regard, most of the removed things are stylistic, only a handful of actual lint rules are missing now. |
`eslint-plugin-import`'s `flatConfigs.recommended` enables both `import/no-named-as-default` and `import/no-named-as-default-member` at warning level. These rules trigger ~1100 false positives on common TypeScript patterns (namespace re-exports, libraries like `zod` / `async` whose default and named exports overlap). The previous airbnb-base setup declared both as `error` but they were silently no-op because FlatCompat doesn't fully register eslint-plugin-import's plugin context. The new config registers the plugin properly, so the rules suddenly start firing — which is a regression for downstream consumers despite being more faithful to airbnb's stated intent. Match the effective baseline behavior by explicitly turning both off. Validated against apify-core: drops 1107 false-positive warnings.
Exposes the airbnb-style stylistic ruleset as a separate opt-in entry
point for consumers who don't want to use a formatter (Prettier, dprint,
Biome). All 58 stylistic rules airbnb-base used to enforce — `indent`,
`quotes`, `semi`, `comma-dangle`, `space-before-blocks`, `keyword-spacing`,
`max-len`, `arrow-parens`, etc. — rewritten as `@stylistic/*` rules so
they work with eslint v9 + flat config, with airbnb's exact options
preserved.
`@stylistic/eslint-plugin` is added as an optional peer dependency so
consumers who don't import the style config don't need to install it.
Usage:
import apifyTypescriptConfig from '@apify/eslint-config/ts';
import apifyStyleConfig from '@apify/eslint-config/style';
export default [
...apifyTypescriptConfig,
...apifyStyleConfig,
];
README updated with the new export and a usage example. Note: not
recommended for projects already using Prettier — the rules will
conflict.
|
In the end, I also added opt-in stylistic rules support to ease the migration: import apifyTypescriptConfig from '@apify/eslint-config/ts';
import apifyStyleConfig from '@apify/eslint-config/style';
export default [
...apifyTypescriptConfig,
...apifyStyleConfig,
]; |
Same root cause as the previous fix for `import/no-named-as-default(-member)`: this rule is part of `import/recommended` and was declared as `error` in airbnb-base, but was silently no-op in the legacy FlatCompat-based config because the plugin context was never registered. The new flat-config setup registers the plugin properly and the rule starts firing. `import/namespace` cannot validate computed property access against imported namespaces (e.g. `REGEXS[someKey]`), producing false positives on legitimate code. Validated against three more downstream consumers: - apify-shared-js: drops 4 false positives in test/regexs.test.ts - crawlee: drops 1 false positive in browser-pool/puppeteer-controller.ts - apify-docs: no change After this fix, all three consumers (plus actor-templates and apify-core) have zero new errors from this PR's changes.
apify/apify-eslint-config#35 has landed and v2.0.0 is published. v2: - drops the unmaintained `eslint-config-airbnb-base` dep - preserves the meaningful airbnb rules inline - moves stylistic rules to an opt-in `@apify/eslint-config/style` export Bumped in the root package.json and in all 24 templates. The temporary `overrides` workaround for `eslint-config-airbnb-base`'s eslint peer dep (added in 3be0201) is no longer needed and has been removed — the unmaintained dep is gone entirely. Note: v2.0.0 was published less than 24 hours ago, so the first CI run on this commit will hit the `min-release-age=86400` constraint. Once v2.0.0 crosses the 1-day threshold, CI will pass without intervention.
The opt-in `@apify/eslint-config/style` config (added in #35) blindly copied airbnb-base's stylistic options. Several of those options conflict with apify's own preferences, which were already enforced via legacy rules in `index.js`: | rule | airbnb default | apify (legacy `index.js`) | |----------------------------|---------------------------|----------------------------------------| | `indent` | 2 spaces | 4 spaces, `SwitchCase: 1` | | `quotes` | single, no template lits | single, `allowTemplateLiterals: true` | | `max-len` | 100 chars | 160 chars | | `object-curly-newline` | `minProperties: 4` rules | `{ consistent: true }` | | `lines-between-class-members` | `exceptAfterSingleLine: false` | `exceptAfterSingleLine: true` | | `function-paren-newline` | `multiline-arguments` | `'off'` | When a consumer spreads both `apifyTypeScriptConfig` and `apifyStyleConfig` together, the legacy `indent: 4` from `index.js` runs alongside the new `@stylistic/indent: 2` from `style.js`. They have *different rule names*, so neither overrides the other — both fire on every indented line. `eslint --fix` then bounces between the two and gives up with `ESLintCircularFixesWarning`. In `apify-core` this produced ~360k `@stylistic/indent` violations across 59 workspaces — almost all on code that's been written in 4-space indent for years. This commit: 1. Aligns the 6 conflicting `@stylistic/*` rules with apify's pre-existing preferences from `index.js`, so existing apify code passes lint without reformatting the world. 2. Explicitly turns off the legacy eslint stylistic rules (`indent`, `quotes`, `max-len`, `object-curly-newline`, `lines-between-class-members`, `function-paren-newline`) inside `style.js` itself — so when both configs are loaded together, only the `@stylistic/*` versions run and there are no circular fixes. Validated against `apify-core` (~59 workspaces): no spurious `@stylistic/indent` errors.
## Why The opt-in `@apify/eslint-config/style` config (added in #35) blindly copied airbnb-base's stylistic options. Several of those options conflict with apify's *own* preferences, which are already enforced via legacy rules in `index.js` and have been the de-facto Apify code style for years: | rule | airbnb default | apify (legacy in `index.js`) | |-------------------------------|--------------------------------|------------------------------------------| | `indent` | 2 spaces | **4 spaces**, `SwitchCase: 1` | | `quotes` | single, no template literals | single, **`allowTemplateLiterals: true`**| | `max-len` | 100 chars | **160 chars** | | `object-curly-newline` | `minProperties: 4` rules | **`{ consistent: true }`** | | `lines-between-class-members` | `exceptAfterSingleLine: false` | **`exceptAfterSingleLine: true`** | | `function-paren-newline` | `multiline-arguments` | **`'off'`** | When a consumer spreads both configs together: ```js import apifyTypeScriptConfig from '@apify/eslint-config/ts.js'; import apifyStyleConfig from '@apify/eslint-config/style.js'; export default [ ...apifyTypeScriptConfig, // legacy `indent: 4` from index.js ...apifyStyleConfig, // new `@stylistic/indent: 2` from airbnb defaults ]; ``` …the legacy `indent: 4` from `index.js` and the new `@stylistic/indent: 2` from `style.js` **both fire** on every indented line, because they're different rule names — neither overrides the other. `eslint --fix` then bounces between the two opinions and gives up with `ESLintCircularFixesWarning`. I tried this against [`apify-core`](https://github.com/apify/apify-core) (the largest internal consumer) by adding `apifyStyleConfig` to its `eslint.config.mjs`. The result: ``` ~360,000 @stylistic/indent errors across 59 lerna workspaces ~17,000 @stylistic/max-len errors ~5,000 @stylistic/object-curly-newline errors ~1,300 @stylistic/quotes errors ~700 @stylistic/lines-between-class-members errors ~550 @stylistic/function-paren-newline errors ``` Almost all of those are on code that's been written in 4-space indent / 160-col / etc. for years. The `style.js` config was effectively saying "rewrite your entire codebase to airbnb's defaults", which was never the intent. The intent was "preserve the legacy airbnb stylistic ruleset for projects that don't use a formatter, with the same options apify was already using." ## What this PR does 1. **Aligns the 6 conflicting `@stylistic/*` rules with apify's pre-existing preferences** from `index.js`. Existing apify codebases pass lint without reformatting the world. 2. **Explicitly turns off the legacy eslint stylistic rules inside `style.js` itself**: ```js indent: 'off', quotes: 'off', 'max-len': 'off', 'object-curly-newline': 'off', 'lines-between-class-members': 'off', 'function-paren-newline': 'off', ``` This way, when both `index.js` and `style.js` are loaded together, only the `@stylistic/*` versions run and there are no circular fixes. Consumers that use *only* `index.js` (no opt-in style) continue to get the legacy rules unchanged. ## Diff Just `style.js`, +18 / −6 lines: ```diff rules: { + // Disable the legacy eslint stylistic rules that this config replaces + // with their `@stylistic/*` equivalents — otherwise both fire on the same + // code with different opinions and produce circular fixes. + indent: 'off', + quotes: 'off', + 'max-len': 'off', + 'object-curly-newline': 'off', + 'lines-between-class-members': 'off', + 'function-paren-newline': 'off', + '@stylistic/array-bracket-spacing': ['error','never'], ... - '@stylistic/function-paren-newline': ['error','multiline-arguments'], + '@stylistic/function-paren-newline': 'off', ... - '@stylistic/indent': ['error',2,{...huge airbnb config...}], + // Apify projects use 4 spaces, not airbnb's 2 — preserved from + // the legacy `indent` rule in `index.js`. + '@stylistic/indent': ['error', 4, { SwitchCase: 1 }], ... - '@stylistic/lines-between-class-members': ['error','always',{exceptAfterSingleLine:false}], - '@stylistic/max-len': ['error',100,2,{...}], + '@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + '@stylistic/max-len': ['error', { code: 160, ignoreUrls: true, ignoreTemplateLiterals: true }], ... - '@stylistic/object-curly-newline': ['error',{...minProperties: 4 rules...}], + '@stylistic/object-curly-newline': ['error', { consistent: true }], ... - '@stylistic/quotes': ['error','single',{avoidEscape:true}], + '@stylistic/quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }], ``` ## Validation I'll re-run lint against `apify-core` once this branch is published — the expected outcome is that the 360k+ spurious indent errors disappear, and any *real* stylistic violations (which there will be some of, since apify-core has historically had no stylistic enforcement at all on a few directories) are surfaced legitimately. ## Rollout Patch release: `v2.0.1`. No breaking changes — this only affects consumers who opt into `@apify/eslint-config/style`, and for them it makes the rules behave the way they probably already expected. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Why
eslint-config-airbnb-base@15.0.0was published in August 2022 and has been the latest version ever since. It still declares its eslint peer dependency as^7.32.0 || ^8.2.0.We use eslint 9. So every consumer of
@apify/eslint-confighas been gettingERESOLVEpeer-dep warnings on every install for as long as we've been on the v9 line:This package already shipped an
overridesblock in its ownpackage.jsonto paper over the conflict — but npm only honorsoverridesfrom the root package, not from transitive deps. So that override has been a no-op for years and never actually did anything.The warnings were tolerable. The breaking change came when
apify/actor-templatesstarted enabling npm'smin-release-age(1-day) setting in.npmrcto defend against malicious freshly-published packages. Withmin-release-ageset, npm switches to strict peer-dep resolution and the long-standing warning escalates to a hard error:The upstream package is unmaintained — there's no realistic path to a fix there. So the only way forward is to drop the dep.
Approach
Five commits:
feat!: drop unmaintained eslint-config-airbnb-base— replaces the legacycompat.extends('airbnb-base')with the modernjs.configs.recommended+pluginImport.flatConfigs.recommended. Drops the dep, drops the dead@eslint/compatdep, drops the no-opoverridesblock, dropsFlatCompatboilerplate.feat: re-add curated rules from airbnb-base directly— brings back 70 of the meaningful rules airbnb-base used to give us, declared inline with airbnb's exact rule values. Stylistic rules are intentionally not in the base config — they're available via the new opt-instyleexport below.fix: silence noisy import/no-named-as-default(-member) rules— see the validation section.feat: add opt-in @apify/eslint-config/style export— exposes airbnb's stylistic ruleset as a separate opt-in entry point for consumers who don't want a formatter.fix: also silence import/namespace— same root cause as the import/no-named-as-default fix; surfaced during validation against more downstream consumers.What's in the curated base rule set
Grouped here the same way they're grouped (with comments) in the diff:
Modern JS / hygiene (16)
eqeqeq,no-var,prefer-const,prefer-template,prefer-arrow-callback,prefer-rest-params,prefer-spread,prefer-object-spread,prefer-numeric-literals,prefer-exponentiation-operator,object-shorthand,no-useless-rename,no-useless-computed-key,no-useless-concat,no-useless-return,no-object-constructor(modern replacement for the deprecatedno-new-object),no-array-constructorDefensive / bug-catching (24)
no-param-reassign(props allowed foracc,req,res,ctxetc.),consistent-return,default-case,default-case-last,default-param-last,radix,no-throw-literal,no-return-assign,no-unused-expressions,no-shadow(TS files use the typescript-eslint variant viats.js),array-callback-return,no-promise-executor-return,no-template-curly-in-string,no-loop-func,no-constructor-return,no-new,no-new-wrappers,no-self-compare,no-sequences,no-unneeded-ternary,no-nested-ternary,no-lonely-if,no-multi-assign,no-else-return,no-empty-function,prefer-promise-reject-errors,block-scoped-var,vars-on-topSecurity (8)
no-eval,no-implied-eval,no-new-func,no-script-url,no-proto,no-extend-native,no-caller,no-iteratorQuality / readability (13)
dot-notation,guard-for-in,no-alert(warn),no-bitwise,no-labels,no-restricted-globals(onlyisFinite/isNaN),no-multi-str,no-octal-escape,no-undef-init,symbol-description,prefer-regex-literals,new-cap,grouped-accessor-pairsImports (8)
import/first,import/no-cycle,import/no-self-import,import/no-mutable-exports,import/no-useless-path-segments,import/no-absolute-path,import/no-dynamic-require,import/newline-after-importAll rule values (severity + options) match airbnb-base's exact configuration.
Cross-checked against
@eslint/js.configs.recommendedandpluginImport.flatConfigs.recommended: none of the curated rules overlap with what those configs already provide.@eslint/jsrecommended is deliberately conservative — it covers things that are almost always bugs (no-cond-assign,no-dupe-keys,no-debugger,no-unreachable, etc.) but explicitly does not include style/defensive opinions likeeqeqeq,no-var,prefer-const, orconsistent-return. So all 70 curated rules are genuinely additive.Opt-in stylistic rules:
@apify/eslint-config/styleThe base config does not enforce stylistic / formatting rules — Prettier (or any other formatter) is the right tool for those. For projects that don't use a formatter and want to keep the airbnb stylistic ruleset, this PR adds a new opt-in export:
This adds 58 stylistic rules (
indent,quotes,semi,comma-dangle,space-before-blocks,keyword-spacing,max-len,arrow-parens,arrow-spacing,object-curly-spacing,padded-blocks,eol-last,linebreak-style,no-multiple-empty-lines,no-trailing-spaces, etc.) with the same optionseslint-config-airbnb-baseused to enforce, rewritten as@stylistic/*rules so they work with eslint v9 + flat config.@stylistic/eslint-pluginis added as an optional peer dependency — consumers who don't import this config don't need to install it. The README is updated with full setup instructions.Validation
Tested against five real consumers using their actual lint entry points (
npm run lint, not a manualnpx eslintinvocation):The +5 errors in apify-core
All 5 are real new findings from
@eslint/jsrecommended catching genuine bugs:no-constant-binary-expression— patterns likeif (cond || true)andx === y === zinconsole-frontendno-unused-private-class-members—#signalEnd/#endedPromisedefined but unused intests/lib/api_helpers.test.jsThe 4 errors in crawlee
Pre-existing
@typescript-eslint/await-thenableerrors inplaywright-crawlerandpuppeteer-crawler'sclick-elements.ts. They reproduce against the unmodified@apify/eslint-config@1.1.0from npm too — unrelated to this PR.The +78 warnings (across all consumers)
All of them are "Unused eslint-disable directive" reports fired by
reportUnusedDisableDirectives: true(which this config has had on for a while). They surface because consumer codebases have inline// eslint-disable-next-line max-classes-per-file(and similar) comments for rules the new config no longer enables — so the disables are dead. Per-consumer breakdown:max-classes-per-file, 5 ×camelcase, 4 ×newline-per-chained-call, 2 ×valid-typeof, 2 ×quote-props, 1 ×no-restricted-globals, 1 ×no-cond-assign, plus stragglersmax-classes-per-file, 1 ×global-require, 1 genericno-cond-assign, 3 ×no-unreachable-loop, 3 genericglobal-require, 1 ×no-cond-assignThese are not regressions — they're dead inline disables that the consumer can clean up with
eslint --fix(which auto-removes unused disable directives).A note on the import-plugin rules I had to silence
The first iterations of this PR caused +1107 warnings in apify-core, +4 errors in apify-shared-js, and +1 error in crawlee, all from
import/no-named-as-default,import/no-named-as-default-member, andimport/namespace.These three rules are part of
eslint-plugin-import'sflatConfigs.recommended. They were declared aserrorin airbnb-base too — but they were silently no-op in the legacy baseline becauseFlatCompatdoesn't fully registereslint-plugin-import's plugin context. The new config registers the plugin properly (viapluginImport.flatConfigs.recommended), so the rules suddenly start firing — including a lot of false positives:import/no-named-as-default/-member: false positives on namespace re-exports and on libraries likezodandasyncwhose default and named exports overlap.import/namespace: cannot validate computed property access against imported namespaces (e.g.REGEXS[someKey]).Two commits in this PR explicitly silence all three to match the effective baseline behavior:
fix: silence noisy import/no-named-as-default(-member) rulesfix: also silence import/namespaceAfter both fixes, all five validated consumers have zero new errors from this PR beyond the 5 genuine bug catches in apify-core.
Rule diff vs. current
@apify/eslint-config@1.1.0/styleThe 4 added rules are modern @eslint/js recommended additions airbnb-base didn't have:
no-constant-binary-expressionno-empty-static-blockno-new-native-nonconstructor(replaces the removedno-new-symbol)no-object-constructor(replaces the deprecatedno-new-object)The remaining ~100 still-removed rules (assuming the
/styleopt-in is used) break down into:no-buffer-constructor,no-new-symbol,no-native-reassign,valid-jsdoc,require-jsdoc,prefer-reflect,lines-around-directive,func-call-spacing,no-process-env,no-process-exit,no-sync,handle-callback-err,callback-return, etc.@stylistic/eslint-plugin— they no longer exist in the modern stylistic plugin either.no-restricted-imports/no-restricted-properties/no-restricted-modules/no-restricted-exports(airbnb had them on with no config, which is meaningless),no-confusing-arrow(overlaps with prettier andno-mixed-operators).Downstream rollout plan
publish_to_npm.yamlworkflow_dispatch as a major bump (v2.0.0). The rule diff is large enough to warrant it, but the validation above shows the impact is small in practice.apify/actor-templates: bump@apify/eslint-configto^2.0.0in the root and in all 24 templates, and remove the temporaryoverridesworkaround that was added to rootpackage.jsonin apify/actor-templates#750 as a stopgap.apify/apify-core: bump and runeslint --fixto clean up the ~64 dead inline disables, then fix the 5 real new errors (3 ×no-constant-binary-expression, 2 ×no-unused-private-class-members).apify/apify-shared-js: bump and runeslint --fixto clean up 5 dead inline disables. Zero real new findings.apify/crawlee: bump and runeslint --fixto clean up 7 dead inline disables. Zero real new findings (the 4 pre-existingawait-thenableerrors are unrelated to this PR).apify/apify-docs: bump and runeslint --fixto clean up 2 dead inline disables. Zero real new findings.@apify/eslint-config/styleopt-in (see README and the "Opt-in stylistic rules" section above).🤖 Generated with Claude Code