Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Bug: Type for values of SharedConfigs should allow an array #10213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
4 tasks done
trygveaa opened this issue Oct 27, 2024 · 14 comments Β· Fixed by #10217
Closed
4 tasks done

Bug: Type for values of SharedConfigs should allow an array #10213

trygveaa opened this issue Oct 27, 2024 · 14 comments Β· Fixed by #10217
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: utils Issues related to the @typescript-eslint/utils package

Comments

@trygveaa
Copy link
Contributor

Before You File a Bug Report Please Confirm You Have Done The Following...

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have searched for related issues and found none that matched my issue.
  • I have read the FAQ and my problem is not listed.

Relevant Package

utils

Playground Link

No response

Repro Code

const plugin: TSESLint.FlatConfig.Plugin = {
  configs: {},
}

plugin.configs = {
  recommended: [
    {
      plugins: {
        example: plugin,
      },
    },
  ],
}

ESLint Config

No response

tsconfig

No response

Expected Result

The code should type check without errors. The code is a stripped down version of the example given on https://eslint.org/docs/latest/extend/plugins#configs-in-plugins except that I've replaced Object.assign with a direct assignment because Object.assign doesn't type check that it can be assigned.

You can see in that example that they set the recommended config to an array, but that's not allowed by the SharedConfigs type used by TSESLint.FlatConfig.Plugin["configs"]. If you replace TSESLint.FlatConfig.Plugin with ESLint.Plugin you can also see that it type checks without errors.

Actual Result

Type checking complains that an array can't be assigned to the type Config (the type for the values of SharedConfigs).

Additional Info

No response

Versions

package version
@typescript-eslint/utils 8.11.0
TypeScript 5.6.3
ESLint 9.13.0
node 20.17.0
@trygveaa trygveaa added bug Something isn't working triage Waiting for team members to take a look labels Oct 27, 2024
@bradzacher
Copy link
Member

Really we should remove that property entirely.
Unlike the legacy system - there's no actual requirement for a flat config plugin to define anything in any particular way because the configs are consumed explicitly by the user via an import, not implicitly via eslint.

@bradzacher bradzacher added package: utils Issues related to the @typescript-eslint/utils package accepting prs Go ahead, send a pull request that resolves this issue and removed triage Waiting for team members to take a look labels Oct 27, 2024
@trygveaa
Copy link
Contributor Author

While there might not be a strict requirement, I think plugins should expose a common interface, otherwise it would be impractical to use them. The ESLint docs say:

A plugin is a JavaScript object that exposes certain properties to ESLint

  • meta - information about the plugin.
  • configs - an object containing named configurations.
  • rules - an object containing the definitions of custom rules.
  • processors - an object containing named processors.

If you plan to distribute your plugin as an npm package, make sure that the module that exports the plugin object is the default export of your package. This will enable ESLint to import the plugin when it is specified in the command line in the --plugin option.

So they do expect you to structure your plugin in a certain way.

I wanted to use the TSESLint.FlatConfig.Plugin type to ensure that I do export the plugin adhering to this structure.

I found another thing that made it impractical to use this type though. All the properties are optional, so even though I specify them, users of the plugin will see them as optional and have to do a non-null assertion. But I can use e.g. Required<...> to get around that.

trygveaa added a commit to trygveaa/typescript-eslint that referenced this issue Oct 27, 2024
According to the docs for ESLint an array should be allowed here, and
the type used by ESLint in their `ESLint.Plugin` interface is:

    Record<string, Linter.LegacyConfig | Linter.Config | Linter.Config[]> | undefined

So we should also allow an array. The helper function `tseslint.config`
returns an array so without this you're not allowed to use a config
returned by this function as a value in this record.

Fixes typescript-eslint#10213
trygveaa added a commit to trygveaa/typescript-eslint that referenced this issue Oct 27, 2024
According to the docs for ESLint an array should be allowed here, and
the type used by ESLint in their `ESLint.Plugin` interface is:

    Record<string, Linter.LegacyConfig | Linter.Config | Linter.Config[]> | undefined

So we should also allow an array. The helper function `tseslint.config`
returns an array so without this you're not allowed to use a config
returned by this function as a value in this record.

Fixes typescript-eslint#10213
@bradzacher
Copy link
Member

So they do expect you to structure your plugin in a certain way

They don't -- meta, rules, processors are the important ones.
configs allows backwards compat with v8 but it's not necessary.
For example many plugins now are choosing NOT to do a configs prop and instead are doing a /configs export like import recommended from 'eslint-plugin-foo/configs/recommended'.

ESLint generally has recommended people export configs prop with the flat configs named recommended and the legacy configs named recommended-lgacy. Some plugins have instead chosen to name them like flat/recommended or recommended-flat so they don't need to do a breaking change.

There's no real consistency in the ecosystem right now, to be honest.

@trygveaa
Copy link
Contributor Author

trygveaa commented Oct 27, 2024

It's not required, and there may not be consistency in the ecosystem right now, but ESLint do suggest exporting configs in this specific way. The docs say:

You can bundle configurations inside a plugin by specifying them under the configs key.

And doesn't mention another way of exposing configs. Wouldn't it then make sense to have a type that matches this? Using the type/property is optional after all.

I'm just going from what I'm reading on this docs page, so you have more context about this than me, but I would think what that page describes is what the ESLint authors intends.

@trygveaa
Copy link
Contributor Author

I realized now that using this type (TSESLint.FlatConfig.Plugin with configs set to Config | ConfigArray) as the type exported by my plugin wouldn't be very useful, as consumers of the plugin wouldn't know if configs was a single object or an array, and would get type errors when using it unless they narrowed it.

I could still us it with a satisfies clause to ensure what I export is valid though.

@bradzacher
Copy link
Member

This is exactly the problem.
A plugin could export either an object or it could export an array. Both are considered valid - the former is the more common thing as the latter requires the user to spread - so you avoid it unless the config needs multiple elements (like ours do).

There is a lot of variance that is accepted within the ecosystem right now - so at best I'd want to specify like config?: LegacyConfig | FlatConfig | FlatConfigArray as that's what is valid and used across the ecosystem.

@trygveaa
Copy link
Contributor Author

Maybe it would be useful to have a helper function (like tseslint.config) that you pass the config to which verifies that the type is correct and returns the type you passed in? Basically just

function plugin<T extends TSESLint.FlatConfig.Plugin>(plugin: T) {
  return plugin
}

@maks-rafalko
Copy link
Contributor

Found the same issue with typings and to be honest it's very confusing.

I want to highlight that ESLint currently officially says that shared config in a plugin can be either object or array.

This plugin exports a recommended config that is an array with one config object. When there is just one config object, you can also export just the object without an enclosing array.

In order to use a config from a plugin in a configuration file, import the plugin and access the config directly through the plugin object. Assuming the config is an array, use the spread operator to add it into the array returned from the configuration file, like this:

// eslint.config.js
import example from "eslint-plugin-example";

export default [
    ...example.configs.recommended
];

Now, let's imaging that we have 2 shared configs:

  • recommended
  • recommendedTypedChecked

and I (as a developer of a plugin) want to extend recommended from recommendedTypedChecked.

In a FlatConfig format, if you need to extend one config from another, you must use arrays cause there is no extends key anymore:

If your config extends other configs, you can export an array:

const baseConfig = require("./base-config");

module.exports = {
    configs: {
        extendedConfig: [
            baseConfig,
           {
                rules: {
                    "example/rule1": "error",
                    "example/rule2": "error"
                }
            }
        ],
    },
};

Thus, if you have shared configs in a plugin and one of the shared config extends the other, there is no way except returning an array, and this exact case is completely unsupported by typescript-eslint typings right now, because only object is allowed.

So, it's practically impossible right now to have fully typed plugin object without any workaround.

Related piece of the code:

export interface SharedConfigs {
[key: string]: Config;
}

I even tried to find how typescript-eslint/eslint-plugin work with it, but it just uses ClassicConfig instead of FlatConfig at the moment.

@kirkwaiblinger
Copy link
Member

Maybe it would be useful to have a helper function (like tseslint.config) that you pass the config to which verifies that the type is correct and returns the type you passed in? Basically just

function plugin<T extends TSESLint.FlatConfig.Plugin>(plugin: T) {
  return plugin
}

@trygveaa

this is exactly just the satisfies operator, no? plugin satisfies TSESLint.FlatConfig.Plugin

@trygveaa
Copy link
Contributor Author

this is exactly just the satisfies operator, no? plugin satisfies TSESLint.FlatConfig.Plugin

Oh, yeah that's true. Then I guess there's no point in it.

@kirkwaiblinger
Copy link
Member

@maks-rafalko Is there something in your response that is not addressed by the resolution for which this issue is accepting PRs (#10213 (comment))?

@maks-rafalko
Copy link
Contributor

maks-rafalko commented Nov 1, 2024

@kirkwaiblinger sorry, probably I didn't get neither your comment nor the comment you are referencing (for me, it doesn't sound like resolution).

Anyway, I thought suggested config?: LegacyConfig | FlatConfig | FlatConfigArray was just a pseudocode, because the current SharedConfig has completely different structure:

export interface SharedConfigs {
[key: string]: Config;
}

I'm not familiar with the codebase so don't take my words seriously, but I thought that the simplest solution could be like this:

interface SharedConfigs {
-    [key: string]: Config;
+    [key: string]: Config | ConfigArray;
}

does it makes sense? If not, then please elaborate on config?: LegacyConfig | FlatConfig | FlatConfigArray:

  • how it should be used inside the type for a plugin
  • why we are mixing legacy and flat configs here
  • why config is singular but not plural

@trygveaa
Copy link
Contributor Author

trygveaa commented Nov 1, 2024

There is a lot of variance that is accepted within the ecosystem right now - so at best I'd want to specify like config?: LegacyConfig | FlatConfig | FlatConfigArray as that's what is valid and used across the ecosystem.

Including legacy config would mean mixing ClassicConfig and FlatConfig. Do you want to do that? The Plugin type lives inside FlatConfig so I figured it didn't really make sense to include legacy config there.

I'm not familiar with the codebase so don't take my words seriously, but I thought that the simplest solution could be like this:

This is what I have already proposed in PR #10217.

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Nov 1, 2024

Right, exactly, all I'm getting at is that my understanding is that all parties agree that #10217 (which implements the intended meaning of #10213 (comment)) is the appropriate resolution then πŸ‘ So I was double checking that there wasn't an additional, unaccounted-for concern raised in #10213 (comment) that we needed to hash out still. πŸ™‚

@github-actions github-actions bot added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Nov 11, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 11, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: utils Issues related to the @typescript-eslint/utils package
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants