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

Open
sindresorhus opened this Issue Aug 20, 2015 · 120 comments

Comments

Projects
None yet
@sindresorhus
Contributor

sindresorhus commented Aug 20, 2015

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

This comment has been minimized.

Show comment
Hide comment
@eslintbot

eslintbot Aug 20, 2015

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 commented Aug 20, 2015

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 label Aug 20, 2015

@lo1tuma

This comment has been minimized.

Show comment
Hide comment
@lo1tuma

lo1tuma Aug 20, 2015

Member

We already discussed this in #2518 with the conclusion that a
peerDependency is the way to go.
On Aug 20, 2015 9:09 AM, "ESLint Bot" notifications@github.com wrote:

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!


Reply to this email directly or view it on GitHub
#3458 (comment).

Member

lo1tuma commented Aug 20, 2015

We already discussed this in #2518 with the conclusion that a
peerDependency is the way to go.
On Aug 20, 2015 9:09 AM, "ESLint Bot" notifications@github.com wrote:

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!


Reply to this email directly or view it on GitHub
#3458 (comment).

@sindresorhus

This comment has been minimized.

Show comment
Hide comment
@sindresorhus

sindresorhus Aug 20, 2015

Contributor

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.

Contributor

sindresorhus commented Aug 20, 2015

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

This comment has been minimized.

Show comment
Hide comment
@lo1tuma

lo1tuma Aug 20, 2015

Member

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.

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

This comment has been minimized.

Show comment
Hide comment
@sindresorhus

sindresorhus Aug 20, 2015

Contributor

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.

Contributor

sindresorhus commented Aug 20, 2015

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

This comment has been minimized.

Show comment
Hide comment
@feross

feross Aug 20, 2015

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@lo1tuma

lo1tuma Aug 20, 2015

Member

@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.
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

This comment has been minimized.

Show comment
Hide comment
@BYK

BYK Aug 20, 2015

Member

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?

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

This comment has been minimized.

Show comment
Hide comment
@lo1tuma

lo1tuma Aug 20, 2015

Member

@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.

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

This comment has been minimized.

Show comment
Hide comment
@BYK

BYK Aug 20, 2015

Member

@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.

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

This comment has been minimized.

Show comment
Hide comment
@nzakas

nzakas Aug 20, 2015

Member

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.

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

This comment has been minimized.

Show comment
Hide comment
@sindresorhus

sindresorhus Aug 21, 2015

Contributor

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.

Contributor

sindresorhus commented Aug 21, 2015

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

This comment has been minimized.

Show comment
Hide comment
@IanVS

IanVS Aug 21, 2015

Member

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.

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

This comment has been minimized.

Show comment
Hide comment
@nzakas

nzakas Aug 24, 2015

Member

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?
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

This comment has been minimized.

Show comment
Hide comment
@feross

feross Aug 30, 2015

Contributor

@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?

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

This comment has been minimized.

Show comment
Hide comment
@nzakas

nzakas Aug 31, 2015

Member

@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.

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

This comment has been minimized.

Show comment
Hide comment
@joakimbeng

joakimbeng Sep 1, 2015

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.

Contributor

joakimbeng commented Sep 1, 2015

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

This comment has been minimized.

Show comment
Hide comment
@nzakas

nzakas Sep 1, 2015

Member

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

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

This comment has been minimized.

Show comment
Hide comment
@ilyavolodin

ilyavolodin Sep 16, 2015

Member

Related to #3659

Member

ilyavolodin commented Sep 16, 2015

Related to #3659

@davidmason

This comment has been minimized.

Show comment
Hide comment
@davidmason

davidmason Oct 20, 2015

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.

davidmason commented Oct 20, 2015

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.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Dec 21, 2017

Contributor

@jamesarosen that's not "solved"; version conflicts will still have nesting, as they should.

Contributor

ljharb commented Dec 21, 2017

@jamesarosen that's not "solved"; version conflicts will still have nesting, as they should.

@jamesarosen

This comment has been minimized.

Show comment
Hide comment
@jamesarosen

jamesarosen Dec 21, 2017

version conflicts will still have nesting, as they should

Right! I had forgotten about the case where multiple configs might depend on different versions of the same common config.

And that suggests some added benefits of solving this in eslint's module-loader (building on @thibaudcolas's comments above):

  1. the top-level project won't have to do things like eslint-disable foo/shared/no-foo bar/shared/no-foo
  2. eslint's existing extends: ['foo', 'bar'] already determines that bar/shared wins over foo/shared

jamesarosen commented Dec 21, 2017

version conflicts will still have nesting, as they should

Right! I had forgotten about the case where multiple configs might depend on different versions of the same common config.

And that suggests some added benefits of solving this in eslint's module-loader (building on @thibaudcolas's comments above):

  1. the top-level project won't have to do things like eslint-disable foo/shared/no-foo bar/shared/no-foo
  2. eslint's existing extends: ['foo', 'bar'] already determines that bar/shared wins over foo/shared

kevinansfield added a commit to TryGhost/eslint-plugin-ghost that referenced this issue Jan 8, 2018

Add ember config with shared plugin solution
closes #3
- add additional plugins for ember:
	- `eslint-plugin-ember`
	- `eslint-plugin-sort-imports-es6-autofix`
- re-export plugin rules with a `ghost/` prefix to get around dependency issues due to eslint not supporting plugin dependencies in shared configs
	- see eslint/eslint#3458 for full background
	- inside consumer apps, if they need to override any rules of the included plugins they will need to use the `ghost/` prefix, eg. `'ghost/ember/no-jquery': 'error'`
- add `ember` config with the `ember/recommended` ruleset copied over with our `ghost/` prefix added

kevinansfield added a commit to TryGhost/eslint-plugin-ghost that referenced this issue Jan 11, 2018

Add ember config with shared plugin solution
closes #3
- add additional plugins for ember:
	- `eslint-plugin-ember`
	- `eslint-plugin-sort-imports-es6-autofix`
- re-export plugin rules with a `ghost/` prefix to get around dependency issues due to eslint not supporting plugin dependencies in shared configs
	- see eslint/eslint#3458 for full background
	- inside consumer apps, if they need to override any rules of the included plugins they will need to use the `ghost/` prefix, eg. `'ghost/ember/no-jquery': 'error'`
- add `ember` config with the `ember/recommended` ruleset copied over with our `ghost/` prefix added

kevinansfield added a commit to TryGhost/eslint-plugin-ghost that referenced this issue Jan 11, 2018

Add ember config with shared plugin dependency solution (#4)
* Add ember config with shared plugin solution

closes #3
- add additional plugins for ember:
	- `eslint-plugin-ember`
	- `eslint-plugin-sort-imports-es6-autofix`
- re-export plugin rules with a `ghost/` prefix to get around dependency issues due to eslint not supporting plugin dependencies in shared configs
	- see eslint/eslint#3458 for full background
	- inside consumer apps, if they need to override any rules of the included plugins they will need to use the `ghost/` prefix, eg. `'ghost/ember/no-jquery': 'error'`
- add `ember` config with the `ember/recommended` ruleset copied over with our `ghost/` prefix added

* Enable all useful eslint-plugin-ember rules
@zenflow

This comment has been minimized.

Show comment
Hide comment
@zenflow

zenflow Jan 25, 2018

lmao @ the "needs bikeshedding" label!

zenflow commented Jan 25, 2018

lmao @ the "needs bikeshedding" label!

@nzakas

This comment has been minimized.

Show comment
Hide comment
@nzakas

nzakas Jan 25, 2018

Member

Sorry for the time between replies. As many are aware, I've been dealing with health issues and have not been able to keep up with conversations regularly.

@ljharb If I'm reading @thibaudcolas #3458 (comment) correctly, it seems like my suggestion actually worked fine and as expected. The change to rule names is what I explicitly mentioned and will definitely lead to some trouble when changing from another config to a plugin config. That's just the cost of admission for this approach.

@tnrich There is nothing to "take a crack at" in core. There is no accept proposal for solving this right now. I think we'd all be happy to have a solution to this problem in the core, we just haven't been able to come up with one.

For everyone: I don't think this was clear from my previous comments, but I'm not against solving this problem of configs relying on plugin rules. I am against trying to shoehorn that behavior into the current shareable config functionality because it was never intended to be used in this way. I know that's not an answer people will be happy with, but it's the truth.

Shareable configs was something I came up with after we implemented extends in configs, as I realized using require meant that an npm package could be used as well. At the time it seemed like an easy win, but if I had to do it over again, I would have spent more time to flesh out a full plugin proposal instead. I never anticipated shareable configs referencing plugin rules so unfortunately, there's no affordances for that nor is there an easy way to add them.

That's why this issue has been open for so long. It's not that we are putting off implementing, it's that we don't have a design to implement. Currently, this is a problem without a solution except for the plugin method I mentioned earlier (which I know isn't ideal, but at least you can get something working).

Member

nzakas commented Jan 25, 2018

Sorry for the time between replies. As many are aware, I've been dealing with health issues and have not been able to keep up with conversations regularly.

@ljharb If I'm reading @thibaudcolas #3458 (comment) correctly, it seems like my suggestion actually worked fine and as expected. The change to rule names is what I explicitly mentioned and will definitely lead to some trouble when changing from another config to a plugin config. That's just the cost of admission for this approach.

@tnrich There is nothing to "take a crack at" in core. There is no accept proposal for solving this right now. I think we'd all be happy to have a solution to this problem in the core, we just haven't been able to come up with one.

For everyone: I don't think this was clear from my previous comments, but I'm not against solving this problem of configs relying on plugin rules. I am against trying to shoehorn that behavior into the current shareable config functionality because it was never intended to be used in this way. I know that's not an answer people will be happy with, but it's the truth.

Shareable configs was something I came up with after we implemented extends in configs, as I realized using require meant that an npm package could be used as well. At the time it seemed like an easy win, but if I had to do it over again, I would have spent more time to flesh out a full plugin proposal instead. I never anticipated shareable configs referencing plugin rules so unfortunately, there's no affordances for that nor is there an easy way to add them.

That's why this issue has been open for so long. It's not that we are putting off implementing, it's that we don't have a design to implement. Currently, this is a problem without a solution except for the plugin method I mentioned earlier (which I know isn't ideal, but at least you can get something working).

@adamscybot

This comment has been minimized.

Show comment
Hide comment
@adamscybot

adamscybot Mar 26, 2018

I have identified another fix for this issue which does not require writing a plugin. We wanted a centralised configuration shared across projects and managed to achieve it like so. This solution is only really suitable if you control the projects consuming the shared config.

  1. Create your shared configuration module (called whatever you like -- its not required to be "eslint-config-[name]" ) and include your eslint configuration (eslint.config.js) as a JS file like this example. Nothing special, it's just your eslint config exported.
module.exports = {
  extends: ['standard', 'plugin:react/recommended', 'prettier', 'prettier/react', 'prettier/standard'],
  plugins: ['react', 'prettier', 'standard'],
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  env: {
    es6: true,
    node: true,
    jest: true
  },
  rules: {
    'prettier/prettier': 'error',
    'react/no-unused-prop-types': 'error'
  }
}
  1. Require all your stuff as dependencies (not devDependencies), including eslint itself in the package.json of this shared configuration module. Take note that we have re-linked the eslint binary from node_modules. This is key. It means eslint searches for plugins/configs in the node_modules of your shared config package, rather than the project consuming it:
{
  "name": "my-common-linting",
  "version": "0.1.0",
  "bin": {
    "eslint": "./node_modules/eslint/bin/eslint.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "babel-eslint": "8.2.2",
    "eslint": "4.19.0",
    "prettier": "1.11.1",
    "eslint-plugin-import": "2.9.0",
    "eslint-plugin-node": "6.0.1",
    "eslint-plugin-prettier": "2.6.0",
    "eslint-plugin-promise": "3.7.0",
    "eslint-plugin-react": "7.7.0",
    "eslint-plugin-standard": "3.0.1",
    "eslint-config-prettier": "2.9.0",
    "eslint-config-standard": "11.0.0"
  }
}
  1. In your project that you want to lint, create an .eslintrc.js file that looks like this:
module.exports = require('my-common-config/eslint.config.js')	
  1. In your project that you want to lint, add an npm run script to execute the linting:
{
    "scripts": {
         "lint": "eslint ."
     }
}
  1. Plugins like ESLint for VSCode need to be told where your eslint binary is, because it can't be found in the top level of your project. For the VSCode case, you can create a project settings file that sets the eslint.nodePath to ./node_modules/my-common-linting/node_modules. You can then commit these editor settings alongside your code. I'm not sure about other editors like Atom.

One thing you lose with this solution is the ability to easily override rules at a project level. But you could conceivably solve this by making eslint.config.js export a function where you can pass in additional config.

Its also not a solution for an uncontrolled environment where you want an easily-consumed library (because of the editor configuration required). Though I think you could just set eslint as a peerDependency and it could possibly work. Though, still, it means you break out of the normal "extends" pattern so this solution is not suitable for library authors.

None of this is ideal, but it works for my simple case of wanting to share configuration amongst internal projects and have the config and the versions of the plugins centrally controlled.

adamscybot commented Mar 26, 2018

I have identified another fix for this issue which does not require writing a plugin. We wanted a centralised configuration shared across projects and managed to achieve it like so. This solution is only really suitable if you control the projects consuming the shared config.

  1. Create your shared configuration module (called whatever you like -- its not required to be "eslint-config-[name]" ) and include your eslint configuration (eslint.config.js) as a JS file like this example. Nothing special, it's just your eslint config exported.
module.exports = {
  extends: ['standard', 'plugin:react/recommended', 'prettier', 'prettier/react', 'prettier/standard'],
  plugins: ['react', 'prettier', 'standard'],
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  env: {
    es6: true,
    node: true,
    jest: true
  },
  rules: {
    'prettier/prettier': 'error',
    'react/no-unused-prop-types': 'error'
  }
}
  1. Require all your stuff as dependencies (not devDependencies), including eslint itself in the package.json of this shared configuration module. Take note that we have re-linked the eslint binary from node_modules. This is key. It means eslint searches for plugins/configs in the node_modules of your shared config package, rather than the project consuming it:
{
  "name": "my-common-linting",
  "version": "0.1.0",
  "bin": {
    "eslint": "./node_modules/eslint/bin/eslint.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "babel-eslint": "8.2.2",
    "eslint": "4.19.0",
    "prettier": "1.11.1",
    "eslint-plugin-import": "2.9.0",
    "eslint-plugin-node": "6.0.1",
    "eslint-plugin-prettier": "2.6.0",
    "eslint-plugin-promise": "3.7.0",
    "eslint-plugin-react": "7.7.0",
    "eslint-plugin-standard": "3.0.1",
    "eslint-config-prettier": "2.9.0",
    "eslint-config-standard": "11.0.0"
  }
}
  1. In your project that you want to lint, create an .eslintrc.js file that looks like this:
module.exports = require('my-common-config/eslint.config.js')	
  1. In your project that you want to lint, add an npm run script to execute the linting:
{
    "scripts": {
         "lint": "eslint ."
     }
}
  1. Plugins like ESLint for VSCode need to be told where your eslint binary is, because it can't be found in the top level of your project. For the VSCode case, you can create a project settings file that sets the eslint.nodePath to ./node_modules/my-common-linting/node_modules. You can then commit these editor settings alongside your code. I'm not sure about other editors like Atom.

One thing you lose with this solution is the ability to easily override rules at a project level. But you could conceivably solve this by making eslint.config.js export a function where you can pass in additional config.

Its also not a solution for an uncontrolled environment where you want an easily-consumed library (because of the editor configuration required). Though I think you could just set eslint as a peerDependency and it could possibly work. Though, still, it means you break out of the normal "extends" pattern so this solution is not suitable for library authors.

None of this is ideal, but it works for my simple case of wanting to share configuration amongst internal projects and have the config and the versions of the plugins centrally controlled.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Mar 27, 2018

Contributor

@adamscybot this seems like it’s a race condition between whether your package, or eslint itself, installs and creates its “bin” first.

Contributor

ljharb commented Mar 27, 2018

@adamscybot this seems like it’s a race condition between whether your package, or eslint itself, installs and creates its “bin” first.

@adamscybot

This comment has been minimized.

Show comment
Hide comment
@adamscybot

adamscybot Mar 27, 2018

@ljharb In the solution, eslint is only included in the shared package, not in the project that is being linted. Since npm only links binaries from direct dependencies (afaik?) there is no race condition.

If I’m wrong about that though you could just give is a different name.

adamscybot commented Mar 27, 2018

@ljharb In the solution, eslint is only included in the shared package, not in the project that is being linted. Since npm only links binaries from direct dependencies (afaik?) there is no race condition.

If I’m wrong about that though you could just give is a different name.

@j-f1

This comment has been minimized.

Show comment
Hide comment
@j-f1

j-f1 Mar 27, 2018

Contributor

File tree to visualize:

my-project
└─ node_modules
   ├─ .bin
   │  └─ eslint -> ../my-common-config/node_modules/eslint/bin/eslint.js
   └─ my-common-config
      ├─ eslint.config.js
      └─ node_modules
         ├─ .bin
         │  └─ eslint -> ../eslint/bin/eslint.js
         └─ eslint
Contributor

j-f1 commented Mar 27, 2018

File tree to visualize:

my-project
└─ node_modules
   ├─ .bin
   │  └─ eslint -> ../my-common-config/node_modules/eslint/bin/eslint.js
   └─ my-common-config
      ├─ eslint.config.js
      └─ node_modules
         ├─ .bin
         │  └─ eslint -> ../eslint/bin/eslint.js
         └─ eslint
@transitive-bullshit

This comment has been minimized.

Show comment
Hide comment
@transitive-bullshit

transitive-bullshit Aug 8, 2018

I would encourage future feedback to continue in #10643.

transitive-bullshit commented Aug 8, 2018

I would encourage future feedback to continue in #10643.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment