Repository: okonet/lint-staged
Pull request #464 (2018-06-08 00:09:08)

feat: Resolve a npm package passed as `--config`
feat: Resolve a npm package passed as `--config`

This PR enables a npm package to be passed to the command line `--config` parameter and will be resolved by lint-staged.

Example:

Say you have a npm package `my-tools-config/lint-staged`, your application can now depend on the configuration package and lint-staged allowing:

```sh
lint-staged --config my-tools-config/lint-staged
```

codecov[bot]: # [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=h1) Report
> Merging [#464](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=desc) into [master](https://codecov.io/gh/okonet/lint-staged/commit/3d0ccb29f24e392e7f1befff8444cd9971b0f101?src=pr&el=desc) will **increase** coverage by `0.04%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/okonet/lint-staged/pull/464/graphs/tree.svg?width=650&height=150&src=pr&token=LSoEgsjk4O)](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #464      +/-   ##
==========================================
+ Coverage   98.72%   98.76%   +0.04%     
==========================================
  Files          12       12              
  Lines         235      243       +8     
  Branches       28       31       +3     
==========================================
+ Hits          232      240       +8     
  Misses          3        3
```


| [Impacted Files](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.js](https://codecov.io/gh/okonet/lint-staged/pull/464/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `93.18% <100%> (+1.51%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=footer). Last update [3d0ccb2...18f3a31](https://codecov.io/gh/okonet/lint-staged/pull/464?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

okonet: I’m wondering what are you trying to solve? 🚫💩 Lint-staged is meant to run on pre-commit so I’m not sure why it needs to be exposed to Node. 
cameronhunter: I understand your concern. We work on multiple applications using the same toolchain, it's a similar(-ish) setup to how [Create React App](https://github.com/facebook/create-react-app) encapsulates tools. Due to the reuse of our toolchain we keep our configuration files in a private npm module, `tool-config`.

Our `pre-commit` hook looks like:

```javascript
#!/usr/bin/env node

const lintStaged = require('lint-staged');
const config = require('tool-config/lint-staged');

lintStaged(config);
```

This allows our configuration to be anywhere on the node resolution path, removing an absolute file path ([that we had previously](https://github.com/okonet/lint-staged/pull/304)) and removes any breakage that would happen in a yarn monorepo (due to module hoisting).
okonet: I’m just wondering is similar is possible with just using node’s resolve inside that file? I’m not against this change but before we go this route is like to check if it’s absolutely necessary. Also I’d like to avoid breaking changes if possible. Thoughts?
cameronhunter: I guess we could use `require.resolve` on the `--config` param within lint-staged. It would allow us to change our pre-commit hook to:

```sh
#!/usr/bin/sh
$(npm bin)/lint-staged --config 'tools-config/lint-staged'
```

With regards to the break change: I was perhaps being overly cautious. I reordered the params on the internal interface which was never explicitly exposed (via `package.json`'s `main` param). It would be a breaking change to people deep requiring into the module, e.g.:

```javascript
const lintStaged = require('lint-staged/src/index');
```

But if they're doing that then this PR would be what they'd want to update to anyway.
okonet: So I’m still not sure if this PR is needed :) can you confirm you can aichive this without it? Or it is still needed for your case?
cameronhunter: Hi @okonet, I've updated the PR. I'm now using `require.resolve` on the `--config` parameter which enables a npm package to be used as config (as well as a file path as before).

This is no longer a breaking change and would only require a minor version bump.
okonet: Can you please add a test for it?
cameronhunter: Test added

Patch:
diff --git a/README.md b/README.md
index 7731e36..fe734c4 100644
--- a/README.md
+++ b/README.md
@@ -71,12 +71,12 @@ $ ./node_modules/.bin/lint-staged --help
   Options:
 
     -V, --version        output the version number
-    -c, --config [path]  Path to configuration file
+    -c, --config [path]  Configuration file path or package
     -d, --debug          Enable debug mode
     -h, --help           output usage information
 ```
 
-* **`--config [path]`**: This can be used to manually specify the `lint-staged` config file location. However, if the specified file cannot be found, it will error out instead of performing the usual search.
+* **`--config [path]`**: This can be used to manually specify the `lint-staged` config file location. However, if the specified file cannot be found, it will error out instead of performing the usual search. You may pass a npm package name for configuration also.
 * **`--debug`**: Enabling the debug mode does the following:
   * `lint-staged` uses the [debug](https://github.com/visionmedia/debug) module internally to log information about staged files, commands being executed, location of binaries etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`.
   * Use the [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`.
diff --git a/__mocks__/my-lint-staged-config/index.js b/__mocks__/my-lint-staged-config/index.js
new file mode 100644
index 0000000..d8a754d
--- /dev/null
+++ b/__mocks__/my-lint-staged-config/index.js
@@ -0,0 +1,5 @@
+'use strict'
+
+module.exports = {
+  '*': 'mytask'
+}
diff --git a/__mocks__/my-lint-staged-config/package.json b/__mocks__/my-lint-staged-config/package.json
new file mode 100644
index 0000000..12614ac
--- /dev/null
+++ b/__mocks__/my-lint-staged-config/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "my-lint-staged-config",
+  "version": "0.0.0"
+}
diff --git a/src/index.js b/src/index.js
index 2e634b1..b455b51 100644
--- a/src/index.js
+++ b/src/index.js
@@ -18,20 +18,28 @@ if (process.stdout.isTTY) {
 
 const errConfigNotFound = new Error('Config could not be found')
 
+function resolveConfig(configPath) {
+  try {
+    return require.resolve(configPath)
+  } catch (ignore) {
+    return configPath
+  }
+}
+
 function loadConfig(configPath) {
   const explorer = cosmiconfig('lint-staged', {
     searchPlaces: [
       'package.json',
-      `.lintstagedrc`,
-      `.lintstagedrc.json`,
-      `.lintstagedrc.yaml`,
-      `.lintstagedrc.yml`,
-      `.lintstagedrc.js`,
-      `lint-staged.config.js`
+      '.lintstagedrc',
+      '.lintstagedrc.json',
+      '.lintstagedrc.yaml',
+      '.lintstagedrc.yml',
+      '.lintstagedrc.js',
+      'lint-staged.config.js'
     ]
   })
 
-  return configPath ? explorer.load(configPath) : explorer.search()
+  return configPath ? explorer.load(resolveConfig(configPath)) : explorer.search()
 }
 
 /**


Test patch:
diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap
index 8f1cdd4..dd16819 100644
--- a/test/__snapshots__/index.spec.js.snap
+++ b/test/__snapshots__/index.spec.js.snap
@@ -1,5 +1,24 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`lintStaged should load an npm config package when specified 1`] = `
+"
+LOG Running lint-staged with the following config:
+LOG {
+  linters: {
+    '*': 'mytask'
+  },
+  concurrent: true,
+  chunkSize: 9007199254740991,
+  globOptions: {
+    matchBase: true,
+    dot: true
+  },
+  ignore: [],
+  subTaskConcurrency: 1,
+  renderer: 'verbose'
+}"
+`;
+
 exports[`lintStaged should load config file when specified 1`] = `
 "
 LOG Running lint-staged with the following config:
diff --git a/test/index.spec.js b/test/index.spec.js
index 1eb8bfe..353beba 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -49,6 +49,13 @@ describe('lintStaged', () => {
     expect(logger.printHistory()).toMatchSnapshot()
   })
 
+  it('should load an npm config package when specified', async () => {
+    expect.assertions(1)
+    jest.mock('my-lint-staged-config')
+    await lintStaged(logger, 'my-lint-staged-config', true)
+    expect(logger.printHistory()).toMatchSnapshot()
+  })
+
   it('should print helpful error message when config file is not found', async () => {
     expect.assertions(1)
     mockCosmiconfigWith(null)