Skip to content

feat: definePlugin #132

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

JoshuaKGoldberg
Copy link
Contributor

@JoshuaKGoldberg JoshuaKGoldberg marked this pull request as ready for review April 23, 2025 19:21
@fasttime fasttime added the Initial Commenting This RFC is in the initial feedback stage label Apr 24, 2025
- Plugin names are often defined in at least four separate places:
- `configs[string].name`, such as `"example/recommended"`
- `configs[string].rules`, such as `{ "example/rule": "error" }`
- `rules[string]`, such as `{ "example/rule": { ... } }`

Choose a reason for hiding this comment

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

When defining the rules objects for a plugin, it doesn't seem like the names are prefixed here. https://eslint.org/docs/latest/extend/plugins#configs-in-plugins

This would just be { "rule": { ... } } and then the example/ would be set when the config registered this plugin as "example": pluginObject.

It would be convenient for this utility to have some way of either automatically prefixing these rules when defining the config, or otherwise enforce that all of the rules exist.

Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg Apr 25, 2025

Choose a reason for hiding this comment

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

Agreed, that would be nice. It would be pretty ergonomic IMO if the new rules could take in an array of rules objects.

The tricky point is, what would assign the rule name for the rules then? Rule meta objects don't have a name field in ESLint core. typescript-eslint's RuleCreator does but that's non-standard.

I would personally be in favor of:

  • Adding an optional meta.name: string property
  • defineConfig allowing an array of rules rather than object iff each has a unique meta.name

Copy link

@molisani molisani Apr 25, 2025

Choose a reason for hiding this comment

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

I don't think there's anything wrong with having to define the rules as a map of "non-namespaced name" to rule object. It's when we have to add the namespace to specify the rules in the configs that this gets potentially messy.

I like the approach of having meta.docs.recommended to determine the recommended config, but perhaps that could go all the way to its conclusion: each rule could locally define (meta) which configs it should be in?

Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg May 1, 2025

Choose a reason for hiding this comment

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

each rule could locally define (meta) which configs it should be in?

Yeah! That's what I'm going for with meta.docs.recommended. The first version of this RFC just has true support for a recommended. From feedback in https://github.com/eslint/rfcs/pull/132/files#r2059307675 I'm expanding it now to also support valid severities such as "error" and "warn". I suspect going further than that would get into tricky territory. Some plugins define specific configs as supersets of others, and having to describe all of those in each individual rule's meta is a lot of duplicate & heavy work.

- `recommended`: a config array of a single object containing:
- `name`: per [configuration naming conventions](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-naming-conventions)
- `plugins`: an object keying `meta.name` to the plugin object
- `rules`: all rules whose `meta.docs?.recommended === true`, with severity set to `"error"`
Copy link

@michaelfaith michaelfaith Apr 24, 2025

Choose a reason for hiding this comment

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

I'm not sure I fully understand why it would be limited to just rules with error severity. I know some really popular plugins that have rules with severity warn in their recommended config (e.g. eslint-plugin-jest). Idea: If meta.docs.recommended was expanded to be boolean | 'warn' | 'error' (and 1 | 2?), then the logic could go something like

{
//...
  rules: Object.fromEntries(
    Object.entries(plugin.rules)
      .filter(([, rule]) => rule.meta.docs?.recommended)
      .map(([ruleName, rule]) => {
        const recommended = rule.meta.docs.recommended;
        if (["warn", "error"].includes(recommended)) {
          return [ruleName, recommended];
        } else {
          return [ruleName, "error"];
        }
    }),
  )
//...
}

Copy link
Member

Choose a reason for hiding this comment

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

Mind that rules in a config (therecommended config) can be configured with a severity and options, neither of which is expressed in the rule's metadata.

Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg Apr 25, 2025

Choose a reason for hiding this comment

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

In my mind, there are kind of two general paths plugins might want to go down:

  • "Automatic": they don't want to be opinionated about specifics like error vs. warning. They just want it to be set up in a generally standard way.
  • "Opinionated": they do care about things like error vs warning. They need tools to set up their opinions in a straightforward way, but are almost certainly going to need to customize things.

For "automatic" plugins, I think this is an opportunity to set one default behavior that works for the recommended starting plugin shape. Note that this is just the default; it's not a requirement. But it's a good way to enforce (what IMO is already) the default behavior of plugins that users generally expect.

For "opinionated" plugins, they don't have to go with these defaults - they can always configure with the stuff mentioned later on.

In other words: I think these these defaults need to make sense for the common case of plugins, but don't need to encapsulate every common customization (added rule options, non-error severity, etc.).

This stance is not very documented in the RFC right now...

neither of which is expressed in the rule's metadata.

But they could add it to the metadata, right? I think this is actually a not-rare practice. E.g. typescript-eslint does it for customizations to strict configs.

Copy link

@michaelfaith michaelfaith Apr 25, 2025

Choose a reason for hiding this comment

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

For "automatic" plugins, I think this is an opportunity to set one default behavior that works for the recommended starting plugin shape. Note that this is just the default; it's not a requirement. But it's a good way to enforce (what IMO is already) the default behavior of plugins that users generally expect.

I definitely agree that having some default behavior that may not cover all use cases is valuable, but if a super common use case isn't covered, then its value is significantly diminished. Having rules with warn severity in a recommended config is super common, and so I fear that if that isn't covered in some way by the default behavior, then there'd be a lot less people able to use the default behavior.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @michaelfaith here - I don't think autogenerating configs where the severity is "error" and cannot be overridden makes much sense. A concrete case: the @eslint/css plugin has a recommended config where only one rule is set to "warn". It's not a great experience if having one rule that needs to be set to "warn" means I need to fully define the entire config.

It does seem like if we want to go down the route of fully-automatic configs then meta needs a significant overhaul. To that end, it's worth thinking about whether or not a v1 of definePlugin() needs this capability.

Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg May 1, 2025

Choose a reason for hiding this comment

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

My "never use warn" bias is showing 😄. In the latest commit I expanded the default generated config design to allow either true or any valid severity. So having meta.docs.recommended: 'warn' will natively work to set that as the severity in the recommended config. Is that a sufficiently scoped default behavior?

Linking to https://github.com/eslint/rfcs/pull/132/files#r2070777537: I suspect going further than that would get into tricky territory. Some plugins define specific configs as supersets of others, and having to describe all of those in each individual rule's meta is a lot of duplicate & heavy work.

Choose a reason for hiding this comment

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

That seems like a good balance. 👍

In the latest commit...

Was that pushed? I'm not seeing that update

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Blagh sorry, just pushed now.

Choose a reason for hiding this comment

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

Looks good!

This RFC's scope intentionally does include a "rule creator" factory akin to [`@typescript-eslint/rule-creator`](https://typescript-eslint.io/developers/custom-rules/#rulecreator).
Creating such a factory in ESLint could be useful but does not seem to be required for any of the problems `definePlugin` aims to solve.
Should a "rule creator" factory be tackled separately?

Choose a reason for hiding this comment

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

This seems like an adjacent but separate conversation.

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

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

Thanks for all the work and research you put into this.

At a high-level, I can see the value of a definePlugin() helper function that:

  • provides type safety
  • makes it easier to define configs
  • enforces best practices (requiring meta.name, meta.version, and meta.namspace

I'm less convinced with some of the more ambitious parts of this proposal such as:

  • lazy-loading rules
  • autocreation of configs

By way of example, defineConfig()s goals were primary to provide type safety and add an extends key, so I'm wondering if maybe it makes sense to scale down definePlugin()'s MVP?

- `recommended`: a config array of a single object containing:
- `name`: per [configuration naming conventions](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-naming-conventions)
- `plugins`: an object keying `meta.name` to the plugin object
- `rules`: all rules whose `meta.docs?.recommended === true`, with severity set to `"error"`
Copy link
Member

Choose a reason for hiding this comment

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

I agree with @michaelfaith here - I don't think autogenerating configs where the severity is "error" and cannot be overridden makes much sense. A concrete case: the @eslint/css plugin has a recommended config where only one rule is set to "warn". It's not a great experience if having one rule that needs to be set to "warn" means I need to fully define the entire config.

It does seem like if we want to go down the route of fully-automatic configs then meta needs a significant overhaul. To that end, it's worth thinking about whether or not a v1 of definePlugin() needs this capability.

import packageData from "../package.json" with { type: "json" };
import { rules } from "./rules/index.js";
export const plugin = definePlugin({
Copy link
Member

Choose a reason for hiding this comment

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

A downside of this approach is that you can't tell just by looking at this code whether or not the plugin exports any configs.

Comment on lines 159 to 163
- `null | undefined`: to preserve that value
- An array containing any number of elements, each either:
- A rule object, in which case it will be turned into a namespaced rule based on the plugin's `meta.name` and the rule's key under the plugin's `rules`
- An array containing a rule object and a severity to specify that rule as
- An object with string keys and severity values, which will be directly merged into the resultant config `rules`
Copy link
Member

Choose a reason for hiding this comment

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

Can you provide some examples for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • A rule object ...: the typescript-eslint equivalent
  • An array containing a rule object ...: the eslint-plugin-storybook equivalent
  • An object with string keys and ...: the eslint-plugin-markdown equivalent

Are those what you're looking for?

Copy link
Member

Choose a reason for hiding this comment

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

More looking for actual code examples.

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've come back to this comment a few times and have failed to parse each time. For "examples" / "actual code examples", the code blocks later on in the doc are what I would think to post. What info is being requested that's not being provided already?

Copy link
Member

Choose a reason for hiding this comment

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

I'd like examples right in this spot. The wording here is confusing and I can't really map each bullet point to any specific example below.

  • An array containing a rule object and a severity to specify that rule as

Are you saying an array of arrays? It's unclear from the context. Imagine instead you put:

  • An array containing a rule object and a severity to specify that rule as, for example: ...

And the example was right there. That's what I'm looking for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, makes sense!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking back at this, wow, yes, it was not clear. I overhauled the examples and explanations in f8e6acf.

JoshuaKGoldberg and others added 2 commits May 1, 2025 16:37
Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>
Co-authored-by: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com>
@JoshuaKGoldberg
Copy link
Contributor Author

JoshuaKGoldberg commented Jun 16, 2025

I'm less convinced with some of the more ambitious parts of this proposal such as:

Added:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Initial Commenting This RFC is in the initial feedback stage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants