Skip to content
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

Support having plugins as dependencies in shareable config #3458

Closed
sindresorhus opened this issue Aug 20, 2015 · 208 comments
Closed

Support having plugins as dependencies in shareable config #3458

sindresorhus opened this issue Aug 20, 2015 · 208 comments
Assignees
Labels
backlog core Relates to ESLint's core APIs and features evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion needs bikeshedding Minor details about this change need to be discussed

Comments

@sindresorhus
Copy link
Contributor

My shareable config uses rules from an external plugin and I would like to make it a dependency so the user doesn't have to manually install the plugin manually. I couldn't find any docs on this, but it doesn't seem to work, so I'll assume it's not currently supported.

module.js:338
    throw err;
          ^
Error: Cannot find module 'eslint-plugin-no-use-extend-native'
    at Function.Module._resolveFilename (module.js:336:15)
    at Function.Module._load (module.js:278:25)
    at Module.require (module.js:365:17)
    at require (module.js:384:17)
    at /usr/local/lib/node_modules/eslint/lib/cli-engine.js:106:26
    at Array.forEach (native)
    at loadPlugins (/usr/local/lib/node_modules/eslint/lib/cli-engine.js:97:21)
    at processText (/usr/local/lib/node_modules/eslint/lib/cli-engine.js:182:5)
    at processFile (/usr/local/lib/node_modules/eslint/lib/cli-engine.js:224:12)
    at /usr/local/lib/node_modules/eslint/lib/cli-engine.js:391:26

I assume it's because you only try to load the plugin when the config is finished merging.

Other shareable configs that depend on a plugin instructs the users to manually install the plugin too and they have it in peerDependencies. I find this sub-optimal though and I don't want the users to have to care what plugins my config uses internally.

The whole point of shareable configs is to minimize boilerplate and overhead, so this would be a welcome improvement.

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

@eslintbot
Copy link

Thanks for the issue! We get a lot of issues, so this message is automatically posted to each one to help you check that you've included all of the information we need to help you.

Reporting a bug? Please be sure to include:

  1. The version of ESLint you are using (run eslint -v)
  2. The source code that caused the problem
  3. The configuration you're using (for the rule or your entire config file)
  4. The actual ESLint output complete with line numbers

Requesting a new rule? Please be sure to include:

  1. The use case for the rule - what is it trying to prevent or flag?
  2. Whether the rule is trying to prevent an error or is purely stylistic
  3. Why you believe this rule is generic enough to be included

Requesting a feature? Please be sure to include:

  1. The problem you want to solve (don't mention the solution)
  2. Your take on the correct solution to problem

Including this information in your issue helps us to triage it and get you a response as quickly as possible.

Thanks!

@eslintbot eslintbot added the triage An ESLint team member will look at this issue soon label Aug 20, 2015
@lo1tuma
Copy link
Member

lo1tuma commented Aug 20, 2015

We already discussed this in #2518 with the conclusion that a
peerDependency is the way to go.

@sindresorhus
Copy link
Contributor Author

Ugh, that's such a leaky abstraction. I guess I won't use plugins then...

The user shouldn't have to care what plugins I use for the rules. This is like requiring to manual install of Lodash when you want ESLint. A shareable config is a node module and should act like it.

@lo1tuma
Copy link
Member

lo1tuma commented Aug 20, 2015

A shareable config is a node module and should act like it.

We use require to load shareable configs or plugins, what would make it act more like a node module than that?

This issue should be also solved when using npm version 3 which installs all subdependencies in the top-level node_modules folder.

@sindresorhus
Copy link
Contributor Author

We use require to load shareable configs or plugins, what would make it act more like a node module than that?

Let the shareable config provide the plugin as an object:

module.exports = {
    plugins: [
        require('eslint-plugin-no-use-extend-native')
    ],
    env: {
        node: true
    },
    rules: {
        'comma-dangle': [2, 'never'],
        'no-cond-assign': 2
};

This issue should be also solved when using npm version 3 which installs all sub-dependencies in the top-level node_modules folder.

That's an implementation detail and not always guaranteed. Nobody should ever depend on that. npm@3 promises flatter dependency tree, not flat. If there are conflicts, there will be nesting.

@feross
Copy link
Contributor

feross commented Aug 20, 2015

The option to require the plugin in the config itself would allow users to use my shareable config without needing to manually install two other plugins.

I like this proposal.

@lo1tuma
Copy link
Member

lo1tuma commented Aug 20, 2015

@sindresorhus good point about npm 3, let's forget about this.

I kind of like your proposal, but it has a few problems:

  1. Eslint needs to know the name of the plugin. This could be solved easily by providing an object with the name e.g plugins: [ { 'eslint-plugin-foo' : require('eslint-plugin-foo')} ]
  2. Eslint caches plugins once they are loaded. This could be a problem if you use different shareable configs in different .eslintrc files where the shareable configs require the same plugin, but in a different version. Possible solution would be to avoid caching in this case.

@BYK
Copy link
Member

BYK commented Aug 20, 2015

Possible solution would be to avoid caching in this case.

Or we can prefix plugins provided by shareable configs with the name of the config?

@lo1tuma
Copy link
Member

lo1tuma commented Aug 20, 2015

@BYK how would you reference the rules then? configname/pluginname/rulename? But I guess we would have the same problem if we avoid caching. We can't determine to which version of the plugin the rule belongs to.

That said, I think we should first decide if we want this feature in ESLint.

@BYK
Copy link
Member

BYK commented Aug 20, 2015

@BYK how would you reference the rules then? configname/pluginname/rulename?

Yep.

That said, I think we should first decide if we want this feature in ESLint.

Agreed. Might be worth piggy backing on npm 3 instead of introducing this complexity.

@nzakas
Copy link
Member

nzakas commented Aug 20, 2015

A few things:

  1. npm 3 doesn't solve this problem, the relationship between a config and a plugin remains a peer relationship. The fact that npm 3 flattens dependencies doesn't fundamentally change that relationship, is just an implementation quirk that allows dependencies to be treated as peers in certain situations. That's not a solution, is a gamble.
  2. Configs should not contain executable code, that's very far outside of the responsibilities of configs.
  3. To keep this in context: we are talking about a one-time setup cost rather than ongoing pain.

While I can understand the desire to have one install that works, I don't see a path towards that without introducing a new type of shareable thing that could encapsulate this functionality.

@sindresorhus
Copy link
Contributor Author

Then maybe introduce a universal sharing thing that can contain multiple plugins/configs/whatever. It could even in the future allow extending ESLint in some ways, with hooks, but I don't want to start that discussion. Just showing the possibilities with something like this.

JSCS supports it like this: https://github.com/wealthfront/javascript/blob/f1f976e9c75a8d141ec77a5493d9d965d951d4a6/jscs/index.js

I just want the user to be able to npm install one module and have the needed config and plugins without having to care about how anything works internally. That's the beauty of normal npm packages.

@IanVS
Copy link
Member

IanVS commented Aug 21, 2015

I agree that the current method becomes unwieldy when you begin sharing configs which use other shared configs and/or plugins. For example, the installation instructions for my own personal config (which extends from Standard) is:

npm install --save-dev eslint-plugin-standard eslint-config-standard eslint-config-ianvs 

It would be much nicer UX to only need:

npm install --save-dev eslint-config-ianvs 

That said, I have no idea how that could be accomplished, and in the end it's a pain I can live with until/unless a better solution is found.

@nzakas
Copy link
Member

nzakas commented Aug 24, 2015

We could extend plugins to allow the inclusion of configs, as plugins were always intended to be a dumping ground of stuff. Thoughts:

  1. How do we specify them in extends? eslint-plugin-foo.configs.whatever? Something else?
  2. We could, in theory, just expose the file system so you could do eslint.plugin-foo/configs/whatever.
  3. This is a bit lousy because we lose the nice eslint-config-* convention for configurations, so it ends up blurring what is a configuration and what is not.
  4. What if a config wants to depend on a plugin that it doesn't own? What does that look like?
  5. What is the expected behavior when a plugin is install directly and the same plugin is installed indirectly via another plugin?

@feross
Copy link
Contributor

feross commented Aug 30, 2015

@nzakas

Configs should not contain executable code, that's very far outside of the responsibilities of configs.

Could you elaborate on this? It seems like this is a philosophical rather than practical objection. From a user's perspective, an eslint config is just an npm package that they need to install and extend in their .eslintrc. They don't care if there's executable code in there or not. Why complicate things for users?

@nzakas
Copy link
Member

nzakas commented Aug 31, 2015

@feross Allowing executable objects arbitrarily in configs would complicate things for users. What I'm saying is let's not complicate it by ensuring that configs remain static regardless of their form.

@joakimbeng
Copy link
Contributor

Let the shareable config provide the plugin as an object

👍 would love to have this functionality!

We use require to load shareable configs or plugins, what would make it act more like a node module than that?

The problem is that the plugin name is not left as is, but instead parsed and prepended with eslint-plugin-. If ESLint didn't do this one could have solved the problem by adding plugins by their full paths, e.g. plugins: [path.join(__dirname, 'node_modules', 'eslint-plugin-babel')], not fancy but it would probably work.

@nzakas
Copy link
Member

nzakas commented Sep 1, 2015

We don't have a good answer for this now. We'll revisit once we've finished some 2.0.0 tasks.

@ilyavolodin ilyavolodin added backlog core Relates to ESLint's core APIs and features needs bikeshedding Minor details about this change need to be discussed evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion and removed triage An ESLint team member will look at this issue soon labels Sep 15, 2015
@ilyavolodin
Copy link
Member

Related to #3659

@davidmason
Copy link

It seems as though this is the case for configs as well, unless I am mistaken. For configs at least, is it possible to change how extends are loaded so that nested extends are processed in the module context that they come from?

This could at least solve the issue for configs, which do not have the issue of executable code.

e.g.

  • say I am making a shareable config for my team's projects, eslint-config-myteam
  • I want to base myteam config on another shareable config eslint-config-goodstyles with a few modifications.
  • in one of my team's projects ("myproject"), I npm install eslint eslint-config-myteam and create .eslintrc that extends: myteam
// eslint-config-myteam/index.js
module.exports = {
  extends: 'goodstyles'
}
# myproject/.eslintrc
extends: myteam

So when eslint is processing myproject/.eslintrc and finds extends: myteam it will locate node_modules/eslint-config-myteam.

At the moment I think it blindly reads that in, then fails when it hits the nested extends: goodstyles because that is not available at the top level. Could it instead keep track of which module it found the myteam config in, and if it finds an extends in there, search in that module for the config it extends. There are a few options for how to search:

  1. look only in the specific module that the extending config is from
  2. look first in the specific module that the extending config is from, then look at the higher level if it is not found there
  3. look first in the module where eslint was run, then in the specific module if the config is missing form there

The question is whether people should be able to override configs by name (on purpose or otherwise) in their extending config. Overriding configs by accident would be possible with 3, so I would rule that out. 1 would not allow peer-dependency configs, so I think 2 is the best option - if someone wants to make other configs peer dependencies they can just not include them in their package.json, but there is the option to include them and make life easier for consumers of their shared config.

@jedwards1211
Copy link
Contributor

It's a slap in the face not to allow require.resolve('eslint-plugin-foo') anywhere 'eslint-plugin-foo' is accepted and getting require-d.

@loynoir
Copy link

loynoir commented May 11, 2022

Was using pnpm, this was never a problem with public-hoist-pattern[]=*eslint*.

But, recently I try yarn berry.

Does any one know what's the workaround for yarn berry, the hoist way, or any way to let yarn berry auto install peer dependency?

@mmkal
Copy link

mmkal commented May 11, 2022

I got tired of waiting and dealing with gotchas with the module resolution patch libraries so I wrote a small library that lets you “wrap” dependencies’ rules, so you can add any plugins or configs you like as regular JavaScript dependencies: https://www.npmjs.com/package/eslint-plugin-wrapper

We use it at my work with pnpm (rush) but since it’s playing by eslint’s rules without hacks or low level patches, it should with any package manager/module resolution strategy, feel free to try it out.

Josmithr added a commit to microsoft/FluidFramework that referenced this issue Jun 3, 2022
…onfig (#10410)

Works around eslint limitations by directly requiring base config, leaning on Node's module resolution instead.

Note that this usage pattern is not explicitly supported by eslint, but has worked for the last few major versions, and has been sited as a workaround in the issue tracking this major eslint limitation (see Issue [3458](eslint/eslint#3458)), and is used in a number of large projects including `create-react-app`.

Also note that this pattern sort of assumes that a given package **does not** install its own copies of the plugin packages used by the shared config package. If they do, they will end up getting the version of the plugin **they** specify, rather than the one specified by the shared config package. This follows npm conventions pretty reasonably, but does deviate from standard eslint conventions a bit. Behavior here could be confusing in cases where one version of a plugin contains a rule that an earlier version doesn't (for example).
- Fortunately, since we leverage lerna's `--strict` option when hoisting dependencies, we currently have the guarantee that this problem won't occur :)

Ultimately, I think the benefits here greatly outweigh any potential issues, and I think our other tooling will help prevent them anyways, so I think this is probably the right change. I just wanted to be transparent here about the trade-offs.

Additionally, removing the extraneous plugin dependencies from consumers of the base config revealed some unused plugin deps that have also been removed.
@belgattitude
Copy link

belgattitude commented Jun 7, 2022

Worth sharing this workaround:

PS: you can see an opinionated example usage https://github.com/belgattitude/nextjs-monorepo-example/tree/main/packages/eslint-config-bases

@SevenOutman
Copy link

SevenOutman commented Jul 8, 2022

Is it possible with ESLint 8 now? I notice eslint-config-react-app has removed mentioning of peer dependencies from their installation guide in their ESLint 8 compatible release.

@haykam821
Copy link

@SevenOutman It looks like eslint-config-react-app uses @rushstack/eslint-patch for this functionality.

@SevenOutman
Copy link

@SevenOutman It looks like eslint-config-react-app uses @rushstack/eslint-patch for this functionality.

Ahh, missed that one.

@stefan-toubia

This comment was marked as off-topic.

@yuriy-yarosh
Copy link

yuriy-yarosh commented Sep 3, 2022

I'm getting some issues with yarn berry as well
@loynoir did you figure it out in the end ?

It's obvious that shell can't process any `virtual' dirs with a virtual zip archive resolver, but general pnp compat should work in this specific case, needs some investigation.

ESLint: 8.23.0

Error: Cannot read config file: /home/yura/src/boosh/.yarn/__virtual__/eslint-config-airbnb-virtual-067a933ec7/0/cache/eslint-config-airbnb-npm-19.0.4-a73150c84a-253178689c.zip/node_modules/eslint-config-airbnb/whitespace.js
Error: Command failed: /home/yura/src/boosh/.yarn/__virtual__/eslint-config-airbnb-virtual-067a933ec7/0/cache/eslint-config-airbnb-npm-19.0.4-a73150c84a-253178689c.zip/node_modules/eslint-config-airbnb/whitespace-async.js
/bin/sh: line 1: /home/yura/src/boosh/.yarn/__virtual__/eslint-config-airbnb-virtual-067a933ec7/0/cache/eslint-config-airbnb-npm-19.0.4-a73150c84a-253178689c.zip/node_modules/eslint-config-airbnb/whitespace-async.js: No such file or catalog

Referenced from: /home/yura/src/boosh/.eslintrc.js
    at checkExecSyncError (node:child_process:841:11)
    at execSync (node:child_process:912:15)
    at Object.<anonymous> (/home/yura/src/boosh/.yarn/__virtual__/eslint-config-airbnb-virtual-067a933ec7/0/cache/eslint-config-airbnb-npm-19.0.4-a73150c84a-253178689c.zip/node_modules/eslint-config-airbnb/whitespace.js:54:38)
    at Module._compile (node:internal/modules/cjs/loader:1126:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
    at Object.require$$0.Module._extensions..js (/home/yura/src/boosh/.pnp.cjs:29491:33)
    at Module.load (node:internal/modules/cjs/loader:1004:32)
    at Function.require$$0.Module._load (/home/yura/src/boosh/.pnp.cjs:29331:14)
    at Module.require (node:internal/modules/cjs/loader:1028:19)

@loynoir
Copy link

loynoir commented Sep 3, 2022

@yuriy-yarosh No, I give up trying yarn berry

@nzakas
Copy link
Member

nzakas commented Sep 7, 2022

Hi folks! Just an update that the new ESLint configuration has now been enabled as an experiment. This new config system eliminates the special treatment of shared configs so that you can include plugins as direct dependencies. Read about it here:

https://eslint.org/blog/2022/08/new-config-system-part-1/
https://eslint.org/blog/2022/08/new-config-system-part-2/
https://eslint.org/blog/2022/08/new-config-system-part-3/

This is the long-term solution to the problem in this issue. We know it took a while, but this was a really complex problem that we needed to think through.

Because we now have this solution implemented, I'm closing this issue. Please try out the new config system and leave your feedback in the discussions.

@nzakas nzakas closed this as completed Sep 7, 2022
@eslint eslint locked as resolved and limited conversation to collaborators Sep 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
backlog core Relates to ESLint's core APIs and features evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion needs bikeshedding Minor details about this change need to be discussed
Projects
None yet