KEMBAR78
feat(eslint-plugin): expose flatConfigs as property of default export by TrevorBurnham · Pull Request #82187 · vercel/next.js · GitHub
Skip to content

Conversation

@TrevorBurnham
Copy link
Contributor

@TrevorBurnham TrevorBurnham commented Jul 30, 2025

Currently, the flat configs object is only available as a named export named flatConfig, which means that the usage example on #73873 (where it's used as a property of the default export named flatConfigs) doesn't work.

If you try to use flatConfig as a property of the default export, it works but causes TypeScript errors if the consumer has type checking enabled in their ESLint config:

eslint.config.mjs:180:24 - error TS2339: Property 'flatConfig' does not exist on type '{ rules: { 'google-font-display': RuleModule; 'google-font-preconnect': RuleModule; 'inline-script-id': RuleModule; 'next-script-for-ga': RuleModule; ... 16 more ...; 'no-unwanted-polyfillio': RuleModule; }; configs: { ...; }; }'.

180       eslintPluginNext.flatConfig.recommended,
                           ~~~~~~~~~~

This has caused quite a bit of confusion, as evidenced by activity on #49337 from after that PR was merged. (I shared that confusion for a while!)

This PR attaches flatConfig to the default export as a property named flatConfigs, while continuing to make it available as a named export named flatConfig for backward compatibility. With this change, the flat config is more easily usable like those of other popular ESLint libraries:

// eslint.config.mjs
import js from "@eslint/js";
import reactPlugin from 'eslint-plugin-react';
import eslintPluginNext from '@next/eslint-plugin-next';

export default [
  ...js.configs.recommended,
  ...reactPlugin.configs.flat.recommended,
  ...eslintPluginImportX.flatConfigs.recommended,
]

I've also reorganized the code a bit, and added proper type checking through @eslint/types.

/cc @SukkaW @CHC383 @rakleed @juliolmuller @devjiwonchoi

@ijjk
Copy link
Member

ijjk commented Jul 30, 2025

Allow CI Workflow Run

  • approve CI run for commit: 4364508

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@ijjk
Copy link
Member

ijjk commented Jul 30, 2025

Allow CI Workflow Run

  • approve CI run for commit: c9db095

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

Currently, the flat configs object is only available as a named export
named `flatConfig`, which means that the usage example on vercel#73873 (where
it's used as a property of the default export named `flatConfigs`)
doesn't work.

If you try to use `flatConfig` as a property of the default export, it
works but causes TypeScript errors if the consumer has type checking
enabled in their ESLint config:

```
eslint.config.mjs:180:24 - error TS2339: Property 'flatConfig' does not exist on type '{ rules: { 'google-font-display': RuleModule; 'google-font-preconnect': RuleModule; 'inline-script-id': RuleModule; 'next-script-for-ga': RuleModule; ... 16 more ...; 'no-unwanted-polyfillio': RuleModule; }; configs: { ...; }; }'.

180       eslintPluginNext.flatConfig.recommended,
                           ~~~~~~~~~~
```

This has caused quite a bit of confusion, as evidenced by activity on
vercel#49337 from after that PR
was merged. (I shared that confusion for a while!)

This PR attaches `flatConfig` to the default export as a property named
`flatConfigs`, while continuing to make it available as a named export
named `flatConfig` for backward compatibility. With this change, the
flat config is more easily usable like those of other popular ESLint
libraries:

```
// eslint.config.mjs
import js from "@eslint/js";
import reactPlugin from 'eslint-plugin-react';
import eslintPluginNext from '@next/eslint-plugin-next';

export default [
  ...js.configs.recommended,
  ...reactPlugin.configs.flat.recommended,
  ...eslintPluginImportX.flatConfigs.recommended,
]
```

I've also reorganized the code a bit, and added proper type checking
through `@eslint/types`.
@TrevorBurnham TrevorBurnham force-pushed the eslint-plugin-flatconfigs-property branch from 4364508 to d1ca365 Compare July 30, 2025 11:52
"fast-glob": "3.3.1"
},
"devDependencies": {
"@types/eslint": "9.6.1",
Copy link
Contributor

@vercel vercel bot Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @types/eslint version 9.6.1 provides types for ESLint 9.x, but the project uses ESLint 8.56.0, creating a version mismatch that will cause TypeScript compilation errors.

View Details

Analysis

The package.json file specifies @types/eslint@9.6.1 as a dev dependency while using eslint@8.56.0. This is a major version mismatch where the types are designed for ESLint 9.x API but the actual runtime uses ESLint 8.x API.

The ESLint 8.x to 9.x transition introduced significant breaking changes to the plugin API structure, particularly around how plugins are defined and consumed. The ESLint.Plugin type used in src/index.ts:90 and other ESLint 9.x-specific types like Linter.Config used in src/index.ts:95 may not exist or have different structures in ESLint 8.x.

This mismatch will cause TypeScript compilation failures when building the project, as the types don't align with the actual ESLint runtime API being used. The build script in line 25 runs tsc for type checking, which will fail with this version incompatibility.


Recommendation

Use compatible type definitions by either:

  1. Recommended: Downgrade to @types/eslint@8.56.10 to match the ESLint 8.56.0 runtime version
  2. Alternative: Upgrade eslint to version 9.x (requires verifying compatibility with all rule implementations and the broader Next.js ecosystem)

For option 1, change line 19 to: "@types/eslint": "8.56.10"

Copy link
Contributor

@SukkaW SukkaW left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue you mentioned is caused by your own problematic ES Interop configuration.

It is you who should either use namespace import import * as eslintPluginNext from 'eslint-plugin-next' or use named import import { flatConfigs as eslintNextFlatConfigs } from 'eslint-plugin-next', there is nothing wrong with eslint-plugin-next, there is nothing need to be changed.

@TrevorBurnham
Copy link
Contributor Author

TrevorBurnham commented Jul 30, 2025

either use namespace import import * as eslintPluginNext from 'eslint-plugin-next'

This doesn't work in my eslint.config.mjs:

eslint.config.mjs:180:7 - error TS2322: Type '{ name: string; plugins: { '@next/next': { rules: { 'google-font-display': RuleModule; 'google-font-preconnect': RuleModule; 'inline-script-id': RuleModule; ... 17 more ...; 'no-unwanted-polyfillio': RuleModule; }; configs: { ...; }; }; }; rules: { ...; }; }' is not assignable to type 'InfiniteDepthConfigWithExtends'.
  Type '{ name: string; plugins: { '@next/next': { rules: { 'google-font-display': RuleModule; 'google-font-preconnect': RuleModule; 'inline-script-id': RuleModule; ... 17 more ...; 'no-unwanted-polyfillio': RuleModule; }; configs: { ...; }; }; }; rules: { ...; }; }' is not assignable to type 'ConfigWithExtends'.
    Types of property 'rules' are incompatible.
      Type '{ '@next/next/google-font-display': string; '@next/next/google-font-preconnect': string; '@next/next/next-script-for-ga': string; '@next/next/no-async-client-component': string; '@next/next/no-before-interactive-script-outside-document': string; ... 15 more ...; '@next/next/no-script-component-in-head': string; }' is not assignable to type 'Partial<Record<string, RuleEntry>>'.
        Property ''@next/next/google-font-display'' is incompatible with index signature.
          Type 'string' is not assignable to type 'RuleEntry | undefined'.

180       eslintPluginNext.flatConfig.recommended,
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

or use named import import { flatConfigs as eslintNextFlatConfigs } from 'eslint-plugin-next'

Same TypeScript error:

eslint.config.mjs:180:7 - error TS2322: Type '{ name: string; plugins: { '@next/next': { rules: { 'google-font-display': RuleModule; 'google-font-preconnect': RuleModule; 'inline-script-id': RuleModule; ... 17 more ...; 'no-unwanted-polyfillio': RuleModule; }; configs: { ...; }; }; }; rules: { ...; }; }' is not assignable to type 'InfiniteDepthConfigWithExtends'.
  Type '{ name: string; plugins: { '@next/next': { rules: { 'google-font-display': RuleModule; 'google-font-preconnect': RuleModule; 'inline-script-id': RuleModule; ... 17 more ...; 'no-unwanted-polyfillio': RuleModule; }; configs: { ...; }; }; }; rules: { ...; }; }' is not assignable to type 'ConfigWithExtends'.
    Types of property 'rules' are incompatible.
      Type '{ '@next/next/google-font-display': string; '@next/next/google-font-preconnect': string; '@next/next/next-script-for-ga': string; '@next/next/no-async-client-component': string; '@next/next/no-before-interactive-script-outside-document': string; ... 15 more ...; '@next/next/no-script-component-in-head': string; }' is not assignable to type 'Partial<Record<string, RuleEntry>>'.
        Property ''@next/next/google-font-display'' is incompatible with index signature.
          Type 'string' is not assignable to type 'RuleEntry | undefined'.

180       eslintNextFlatConfigs.recommended,
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Worse, ESLint itself doesn't accept it:

ESLint: 9.29.0

import { flatConfig } from "@next/eslint-plugin-next";
         ^^^^^^^^^^
SyntaxError: Named export 'flatConfig' not found. The requested module '@next/eslint-plugin-next' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '@next/eslint-plugin-next';
const { flatConfig } = pkg;

If I follow the recommendation given by ESLint in that error message, the linter runs:

import eslintPluginNext from "@next/eslint-plugin-next";
const { flatConfig } = eslintPluginNext;

But TypeScript complains, because flatConfig isn't a property of the default export.

@TrevorBurnham
Copy link
Contributor Author

To help clarify the issue, I've spun up a CodeSandbox: https://codesandbox.io/p/devbox/vsg7nz

All I've done to set up the sandbox project is:

  1. Followed the official typescript-eslint start guide
  2. Installed @next/eslint-plugin-next@15.4.5

Take a look at the project's eslint.config.mjs and see if you can find a way to get it working with the ESLint plugin with no TypeScript errors.

Since these types are referenced from the built types, they need to be in the consumer's dependency tree.
require('./rules/no-unwanted-polyfillio') as typeof import('./rules/no-unwanted-polyfillio'),
}

const recommendedRules: Linter.RulesRecord = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesnt preserve the rule names, I had created a pr earlier #82202 that fixes the typing issue while also preserving the rule names in the object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch! I'll switch to using satisfies here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: c9db095

This change also removes the hard dependency on `@types/eslint`.

I've also removed the unused dev dependency on `eslint`.
export const { rules, configs } = plugin
export { flatConfig }
export default { ...plugin, flatConfigs: flatConfig }
export { configs, flatConfig, rules }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 I don't think these should be named exports, as this causes a types mismatch with the fact the plugin is CJS-only so you cannot access these at runtime?

Copy link
Contributor Author

@TrevorBurnham TrevorBurnham Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values (configs, flatConfig, and rules) were already named exports, though. The change from

export const { rules, configs } = plugin
export { flatConfig }

to

export { configs, flatConfig, rules }

amounts to a no-op.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, I follow that, but I think both are wrong? With this, TypeScript will tell you that you can import them as named members, but at runtime you'll get an error that the module is CJS, so you can only use the default export?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you’re right, but I’m wary of making any change that might break backward compatibility for someone, somewhere…

Unless we want to start putting together a new major version? Tbh it’d be great to clean some things up. 🧹

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the non-breaking fix would be to start building this with an actual ESM build, but I defer to the Next.js folks whether they think fixing the types is okay to do w/o a major for the whole of Next.js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is already broken anyway, so any breaking changes are just fixes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the non-breaking fix would be to start building this with an actual ESM build

I like that idea. I generated a proof-of-concept branch (based off of this branch, but it could easily be extracted as a separate change). Claude Sonnet 4 did the legwork, but I've looked over the output and I think it's sane.

I could spin up a new PR for just the ESM build, if that'd be helpful.

@TrevorBurnham
Copy link
Contributor Author

With the deprecation of next lint, I think this issue has become more urgent. Per that announcement:

When creating a new Next.js project, you can now choose between ESLint (comprehensive rules), Biome (fast with fewer rules), or no linter. ESLint projects now generate explicit eslint.config.mjs files instead of relying on the next lint command wrapper, providing complete transparency into your linting rules.

As I've described above, if you enable type checking for that eslint.config.mjs, you'll get errors when trying to use @next/eslint-plugin-next.

I'm happy to help fix the issue, but I could use some guidance from the folks at Vercel to tell me what kind of change they'd like to merge.

@TrevorBurnham
Copy link
Contributor Author

Closing this in favor of #82969, which takes a different approach to solving the same issue. It's a simpler change that generates proper JS and types for ESM, while ensuring that the CJS build is fully backward-compatible.

@github-actions github-actions bot added the locked label Sep 7, 2025
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 7, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants