Repository: prettier/prettier-eslint
Pull request #119 (2017-08-30 16:32:36)

feat: support prettier configuration
This actually is a partial solution to #118.

BREAKING CHANGE: the API is now async 100% of the time. It's a shame, but we have to do that due to the async nature of resolving the prettier config.

I'd really appreciate a review on this. I'm going to publish a `beta` which you can install with `prettier-eslint@beta`. I'll also publish a beta of `prettier-eslint-cli` soon. Please give it a go!

codecov-io: # [Codecov](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=h1) Report
> Merging [#119](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=desc) into [master](https://codecov.io/gh/prettier/prettier-eslint/commit/c1c29a5a2061e0a5b94cafff39f22d169acb5fd4?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/prettier/prettier-eslint/pull/119/graphs/tree.svg?width=650&src=pr&token=SKAX5tKPfD&height=150)](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #119   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           2      2           
  Lines         185    187    +2     
  Branches       31     31           
=====================================
+ Hits          185    187    +2
```


| [Impacted Files](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/utils.js](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=tree#diff-c3JjL3V0aWxzLmpz) | `100% <ø> (ø)` | :arrow_up: |
| [src/index.js](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/prettier/prettier-eslint/pull/119?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/prettier/prettier-eslint/pull/119?src=pr&el=footer). Last update [c1c29a5...ed1042f](https://codecov.io/gh/prettier/prettier-eslint/pull/119?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.babelrc b/.babelrc
index 655ba4e..f468d0c 100644
--- a/.babelrc
+++ b/.babelrc
@@ -5,7 +5,8 @@
       {
         "targets": {
           "node": "4.5"
-        }
+        },
+        "exclude": ["transform-regenerator"]
       }
     ]
   ],
diff --git a/.eslintrc b/.eslintrc
index a70cfb8..164cc7c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -6,6 +6,13 @@
   ],
   "rules": {
     "valid-jsdoc": "off",
-    "max-len": "off"
+    "max-len": "off",
+    "semi": "off",
+    "quotes": "off",
+    "space-before-function-paren": ["error", {
+      "anonymous": "never",
+      "named": "never",
+      "asyncArrow": "always"
+    }],
   }
 }
diff --git a/package.json b/package.json
index aacfd20..b6a57a7 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,15 @@
 {
   "name": "prettier-eslint",
   "version": "0.0.0-development",
-  "description": "Formats your JavaScript using prettier followed by eslint --fix",
+  "description":
+    "Formats your JavaScript using prettier followed by eslint --fix",
   "main": "dist/index.js",
   "scripts": {
     "start": "nps",
     "test": "nps test",
     "precommit": "opt --in pre-commit --exec \"npm start validate\""
   },
-  "files": [
-    "dist"
-  ],
+  "files": ["dist"],
   "keywords": [],
   "author": "Kent C. Dodds <kent@doddsfamily.us> (http://kentcdodds.com/)",
   "license": "MIT",
diff --git a/src/__mocks__/eslint.js b/src/__mocks__/eslint.js
index 160c5f5..57aed9c 100644
--- a/src/__mocks__/eslint.js
+++ b/src/__mocks__/eslint.js
@@ -9,7 +9,7 @@ mockGetConfigForFileSpy.overrides = {}
 const mockExecuteOnTextSpy = jest.fn(mockExecuteOnText)
 
 module.exports = Object.assign(eslint, {
-  CLIEngine: MockCLIEngine,
+  CLIEngine: jest.fn(MockCLIEngine),
   mock: {
     getConfigForFile: mockGetConfigForFileSpy,
     executeOnText: mockExecuteOnTextSpy,
@@ -27,6 +27,7 @@ function MockCLIEngine(...args) {
 
 MockCLIEngine.prototype = Object.create(CLIEngine.prototype)
 
+// eslint-disable-next-line complexity
 function mockGetConfigForFile(filePath) {
   if (mockGetConfigForFileSpy.throwError) {
     throw mockGetConfigForFileSpy.throwError
@@ -42,11 +43,7 @@ function mockGetConfigForFile(filePath) {
         semi: [2, 'never'],
         'max-len': [2, 120, 2],
         indent: [2, 2, {SwitchCase: 1}],
-        quotes: [
-          2,
-          'single',
-          {avoidEscape: true, allowTemplateLiterals: true},
-        ],
+        quotes: [2, 'single', {avoidEscape: true, allowTemplateLiterals: true}],
         'comma-dangle': [
           2,
           {
diff --git a/src/__mocks__/prettier.js b/src/__mocks__/prettier.js
index 173a7d8..64fe975 100644
--- a/src/__mocks__/prettier.js
+++ b/src/__mocks__/prettier.js
@@ -5,7 +5,10 @@ module.exports = prettier
 
 const mockFormatSpy = jest.fn(mockFormat)
 
-Object.assign(prettier, {format: mockFormatSpy})
+Object.assign(prettier, {
+  format: mockFormatSpy,
+  resolveConfig: jest.fn(prettier.resolveConfig),
+})
 
 function mockFormat(...args) {
   global.__PRETTIER_ESLINT_TEST_STATE__.prettierPath = __filename
diff --git a/src/index.js b/src/index.js
index cb6149c..31c0076 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,7 +7,8 @@ import prettyFormat from 'pretty-format'
 import {oneLine, stripIndent} from 'common-tags'
 import indentString from 'indent-string'
 import getLogger from 'loglevel-colored-level-prefix'
-import {getOptionsForFormatting, defaultEslintConfig} from './utils'
+import merge from 'lodash.merge'
+import {getOptionsForFormatting} from './utils'
 
 const logger = getLogger({prefix: 'prettier-eslint'})
 
@@ -38,7 +39,7 @@ module.exports = format
  * @param {Boolean} options.prettierLast - Run Prettier Last
  * @return {String} - the formatted string
  */
-function format(options) {
+async function format(options) {
   const {logLevel = getDefaultLogLevel()} = options
   logger.setLevel(logLevel)
   logger.trace('called format with options:', prettyFormat(options))
@@ -48,14 +49,20 @@ function format(options) {
     text = getTextFromFilePath(filePath),
     eslintPath = getModulePath(filePath, 'eslint'),
     prettierPath = getModulePath(filePath, 'prettier'),
-    prettierOptions,
     prettierLast,
     fallbackPrettierOptions,
   } = options
 
-  const eslintConfig = defaultEslintConfig(
-    getConfig(filePath, eslintPath),
+  const eslintConfig = merge(
+    {},
     options.eslintConfig,
+    getConfig(filePath, eslintPath),
+  )
+
+  const prettierOptions = merge(
+    {},
+    options.prettierOptions,
+    await getPrettierConfig(filePath, prettierPath),
   )
 
   const formattingOptions = getOptionsForFormatting(
@@ -112,20 +119,7 @@ function createPrettify(formatOptions, prettierPath) {
       ${indentString(text, 2)}
     `,
     )
-    let prettier
-    try {
-      logger.trace(`requiring prettier module at "${prettierPath}"`)
-      prettier = require(prettierPath)
-    } catch (error) {
-      logger.error(
-        oneLine`
-        There was trouble getting prettier.
-        Is "prettierPath: ${prettierPath}"
-        a correct path to the prettier module?
-      `,
-      )
-      throw error
-    }
+    const prettier = requireModule(prettierPath, 'prettier')
     try {
       logger.trace(`calling prettier.format with the text and prettierOptions`)
       const output = prettier.format(text, formatOptions)
@@ -225,6 +219,26 @@ function getConfig(filePath, eslintPath) {
   }
 }
 
+function getPrettierConfig(filePath, prettierPath) {
+  const prettier = requireModule(prettierPath, 'prettier')
+  return prettier.resolveConfig(filePath)
+}
+
+function requireModule(modulePath, name) {
+  try {
+    logger.trace(`requiring "${name}" module at "${modulePath}"`)
+    return require(modulePath)
+  } catch (error) {
+    logger.error(
+      oneLine`
+      There was trouble getting "${name}".
+      Is "${modulePath}" a correct path to the "${name}" module?
+    `,
+    )
+    throw error
+  }
+}
+
 function getModulePath(filePath = __filename, moduleName) {
   try {
     return requireRelative.resolve(moduleName, filePath)
@@ -242,17 +256,11 @@ function getModulePath(filePath = __filename, moduleName) {
 }
 
 function getESLintCLIEngine(eslintPath, eslintOptions) {
+  const {CLIEngine} = requireModule(eslintPath, 'eslint')
   try {
-    logger.trace(`requiring eslint module at "${eslintPath}"`)
-    const {CLIEngine} = require(eslintPath)
     return new CLIEngine(eslintOptions)
   } catch (error) {
-    logger.error(
-      oneLine`
-        There was trouble creating the ESLint CLIEngine.
-        Is "eslintPath: ${eslintPath}" a correct path to the ESLint module?
-      `,
-    )
+    logger.error(`There was trouble creating the ESLint CLIEngine.`)
     throw error
   }
 }
diff --git a/src/utils.js b/src/utils.js
index 9a78f74..b1465c0 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,7 +1,6 @@
 import {oneLine} from 'common-tags'
 import delve from 'dlv'
 import getLogger from 'loglevel-colored-level-prefix'
-import merge from 'lodash.merge'
 
 const logger = getLogger({prefix: 'prettier-eslint'})
 const RULE_DISABLED = {}
@@ -42,11 +41,7 @@ const OPTION_GETTERS = {
 }
 
 /* eslint import/prefer-default-export:0 */
-export {getOptionsForFormatting, defaultEslintConfig}
-
-function defaultEslintConfig(eslintConfig = {}, defaultConfig = {}) {
-  return merge({}, defaultConfig, eslintConfig)
-}
+export {getOptionsForFormatting}
 
 function getOptionsForFormatting(
   eslintConfig,


Test patch:
diff --git a/src/__tests__/index.js b/src/__tests__/index.js
index 8cfef68..be0c820 100644
--- a/src/__tests__/index.js
+++ b/src/__tests__/index.js
@@ -176,6 +176,7 @@ beforeEach(() => {
   eslintMock.mock.executeOnText.mockClear()
   eslintMock.mock.getConfigForFile.mockClear()
   prettierMock.format.mockClear()
+  prettierMock.resolveConfig.mockClear()
   fsMock.readFileSync.mockClear()
   loglevelMock.mock.clearAll()
   global.__PRETTIER_ESLINT_TEST_STATE__ = {}
@@ -186,93 +187,89 @@ tests.forEach(({title, modifier, input, output}) => {
   if (modifier) {
     fn = test[modifier]
   }
-  fn(title, () => {
+  fn(title, async () => {
     input.text = stripIndent(input.text).trim()
     const expected = stripIndent(output).trim()
-    const actual = format(input)
+    const actual = await format(input)
     // adding the newline in the expected because
     // prettier adds a newline to the end of the input
     expect(actual).toBe(`${expected}\n`)
   })
 })
 
-test('failure to fix with eslint throws and logs an error', () => {
+test('failure to fix with eslint throws and logs an error', async () => {
   const {executeOnText} = eslintMock.mock
-  const error = 'Something happened'
-  executeOnText.throwError = new Error(error)
-  expect(() => format({text: ''})).toThrowError(error)
-  expect(logger.error).toHaveBeenCalledTimes(1)
+  const error = new Error('Something happened')
+  executeOnText.throwError = error
+  const errorThrown = await format({text: ''}).catch(e => e)
   executeOnText.throwError = null
+
+  expect(errorThrown).toBe(error)
+  expect(logger.error).toHaveBeenCalledTimes(1)
 })
 
-test('logLevel is used to configure the logger', () => {
+test('logLevel is used to configure the logger', async () => {
   logger.setLevel = jest.fn()
-  format({text: '', logLevel: 'silent'})
+  await format({text: '', logLevel: 'silent'})
   expect(logger.setLevel).toHaveBeenCalledTimes(1)
   expect(logger.setLevel).toHaveBeenCalledWith('silent')
 })
 
-test(`when prettier throws, log to logger.error and throw the error`, () => {
-  const {format: prettierMockFormat} = prettierMock
-  const error = 'something bad happened'
-  prettierMockFormat.throwError = new Error(error)
+test(`when prettier throws, log to logger.error and throw the error`, async () => {
+  const error = new Error('something bad happened')
+  prettierMock.format.throwError = error
+  const errorThrown = await format({text: ''}).catch(e => e)
+  prettierMock.format.throwError = null
 
-  expect(() => format({text: ''})).toThrowError(error)
+  expect(errorThrown).toBe(error)
   expect(logger.error).toHaveBeenCalledTimes(1)
-
-  prettierMockFormat.throwError = null
 })
 
-test('can accept a path to an eslint module and uses that instead.', () => {
+test('can accept a path to an eslint module and uses that instead.', async () => {
   const eslintPath = path.join(__dirname, '../__mocks__/eslint')
-  const {executeOnText} = eslintMock.mock
-  format({text: '', eslintPath})
-  expect(executeOnText).toHaveBeenCalledTimes(1)
+  await format({text: '', eslintPath})
+  expect(eslintMock.mock.executeOnText).toHaveBeenCalledTimes(1)
 })
 
-test('fails with an error if the eslint module cannot be resolved.', () => {
+test('fails with an error if the eslint module cannot be resolved.', async () => {
   const eslintPath = path.join(
     __dirname,
     '../__mocks__/non-existant-eslint-module',
   )
 
-  expect(() => format({text: '', eslintPath})).toThrowError(
-    /non-existant-eslint-module/,
-  )
+  const error = await format({text: '', eslintPath}).catch(e => e)
+
+  expect(error.message).toMatch(/non-existant-eslint-module/)
   expect(logger.error).toHaveBeenCalledTimes(1)
 
   const errorString = expect.stringMatching(
-    /ESLint.*?eslintPath.*non-existant-eslint-module/,
+    /trouble getting.*?eslint.*non-existant-eslint-module/,
   )
 
   expect(logger.error).toHaveBeenCalledWith(errorString)
 })
 
-test('can accept a path to a prettier module and uses that instead.', () => {
+test('can accept a path to a prettier module and uses that instead.', async () => {
   const prettierPath = path.join(__dirname, '../__mocks__/prettier')
-  const {format: prettierMockFormat} = prettierMock
-  format({text: '', prettierPath})
-  expect(prettierMockFormat).toHaveBeenCalledTimes(1)
+  await format({text: '', prettierPath})
+  expect(prettierMock.format).toHaveBeenCalledTimes(1)
 })
 
-test('fails with an error if the prettier module cannot be resolved.', () => {
+test('fails with an error if the prettier module cannot be resolved.', async () => {
   const prettierPath = path.join(
     __dirname,
     '../__mocks__/non-existant-prettier-module',
   )
-  expect(() => format({text: '', prettierPath})).toThrowError(
-    /non-existant-prettier-module/,
-  )
+  const error = await format({text: '', prettierPath}).catch(e => e)
+  expect(error.message).toMatch(/non-existant-prettier-module/)
   expect(logger.error).toHaveBeenCalledTimes(1)
-  const errorString = expect.stringMatching(
-    /prettier.*?prettierPath.*non-existant-prettier-module/,
-  )
+  const errorString = expect.stringMatching(/trouble getting.*prettier/)
   expect(logger.error).toHaveBeenCalledWith(errorString)
 })
 
-test('resolves to the eslint module relative to the given filePath', () => {
+test('resolves to the eslint module relative to the given filePath', async () => {
   const filePath = require.resolve('../../tests/fixtures/paths/foo.js')
-  format({text: '', filePath})
+  await format({text: '', filePath})
   const stateObj = {
     eslintPath: require.resolve(
       '../../tests/fixtures/paths/node_modules/eslint/index.js',
@@ -284,9 +281,9 @@ test('resolves to the eslint module relative to the given filePath', () => {
   expect(global.__PRETTIER_ESLINT_TEST_STATE__).toMatchObject(stateObj)
 })
 
-test('resolves to the local eslint module', () => {
+test('resolves to the local eslint module', async () => {
   const filePath = '/blah-blah/default-config'
-  format({text: '', filePath})
+  await format({text: '', filePath})
   expect(global.__PRETTIER_ESLINT_TEST_STATE__).toMatchObject({
     // without Jest's mocking, these would actually resolve to the
     // project modules :) The fact that jest's mocking is being
@@ -296,28 +293,47 @@ test('resolves to the local eslint module', () => {
   })
 })
 
-test('reads text from fs if filePath is provided but not text', () => {
+test('reads text from fs if filePath is provided but not text', async () => {
   const filePath = '/blah-blah/some-file.js'
-  try {
-    format({filePath})
-  } catch (e) {
-    // ignore
-  }
+  await format({filePath}).catch(() => {})
   // one hit to get the file and one for the eslintignore
   expect(fsMock.readFileSync).toHaveBeenCalledTimes(2)
   expect(fsMock.readFileSync).toHaveBeenCalledWith(filePath, 'utf8')
 })
 
-test('logs error if it cannot read the file from the filePath', () => {
+test('logs error if it cannot read the file from the filePath', async () => {
   const originalMock = fsMock.readFileSync
   fsMock.readFileSync = jest.fn(() => {
     throw new Error('some error')
   })
-  expect(() => format({filePath: '/some-path.js'})).toThrowError(/some error/)
+  const error = await format({filePath: '/some-path.js'}).catch(e => e)
+  expect(error.message).toMatch(/some error/)
   expect(logger.error).toHaveBeenCalledTimes(1)
   fsMock.readFileSync = originalMock
 })
 
+test('calls prettier.resolveConfig with the file path', async () => {
+  const filePath = require.resolve('../../tests/fixtures/paths/foo.js')
+  await format({
+    filePath,
+    text: defaultInputText(),
+    eslintConfig: getESLintConfigWithDefaultRules(),
+  })
+  expect(prettierMock.resolveConfig).toHaveBeenCalledTimes(1)
+  expect(prettierMock.resolveConfig).toHaveBeenCalledWith(filePath)
+})
+
+test('logs if there is a problem making the CLIEngine', async () => {
+  const error = new Error('fake error')
+  eslintMock.CLIEngine.mockImplementation(() => {
+    throw error
+  })
+  const errorThrown = await format({text: ''}).catch(e => e)
+  eslintMock.CLIEngine.mockReset()
+  expect(errorThrown).toBe(error)
+  expect(logger.error).toHaveBeenCalledTimes(1)
+})
+
 function getESLintConfigWithDefaultRules(overrides) {
   return {
     parserOptions: {ecmaVersion: 7},
diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js
index 3411237..14a3e2a 100644
--- a/src/__tests__/utils.js
+++ b/src/__tests__/utils.js
@@ -1,96 +1,4 @@
-import {getOptionsForFormatting, defaultEslintConfig} from '../utils'
-
-const defaultEslintConfigTests = [
-  {
-    config: {
-      foo: 'bar',
-      parserOptions: {
-        ecmaVersion: 7,
-        ecmaFeatures: {
-          jsx: true,
-        },
-      },
-      rules: {
-        indent: [2, 2, {SwitchCase: 1}],
-        quotes: [
-          2,
-          'single',
-          {avoidEscape: true, allowTemplateLiterals: true},
-        ],
-      },
-      options: {
-        printWidth: 120,
-        tabWidth: 2,
-      },
-    },
-    defaults: {
-      foo: 'not',
-      fiz: 'fuz',
-      parserOptions: {
-        ecmaVersion: 6,
-        sourceType: 'module',
-        ecmaFeatures: {
-          jsx: false,
-          impliedStrict: true,
-        },
-      },
-      rules: {
-        'max-len': [2, 120, 2],
-        indent: [0, 0, {SwitchCase: 0}],
-      },
-      options: {
-        parser: 'babylon',
-        printWidth: 110,
-      },
-    },
-    result: {
-      foo: 'bar',
-      fiz: 'fuz',
-      parserOptions: {
-        ecmaVersion: 7,
-        sourceType: 'module',
-        ecmaFeatures: {
-          jsx: true,
-          impliedStrict: true,
-        },
-      },
-      rules: {
-        'max-len': [2, 120, 2],
-        indent: [2, 2, {SwitchCase: 1}],
-        quotes: [
-          2,
-          'single',
-          {avoidEscape: true, allowTemplateLiterals: true},
-        ],
-      },
-      options: {
-        parser: 'babylon',
-        printWidth: 120,
-        tabWidth: 2,
-      },
-    },
-  },
-  {
-    config: undefined,
-    defaults: undefined,
-    result: {},
-  },
-  {
-    config: {},
-    defaults: {},
-    result: {},
-  },
-  {
-    config: undefined,
-    defaults: {},
-    result: {},
-  },
-  {
-    config: {},
-    defaults: undefined,
-    result: {},
-  },
-]
+import {getOptionsForFormatting} from '../utils'
 
 const getPrettierOptionsFromESLintRulesTests = [
   {
@@ -219,13 +127,6 @@ const getPrettierOptionsFromESLintRulesTests = [
   {rules: {indent: [2, 'tab']}, options: {useTabs: true}},
 ]
 
-defaultEslintConfigTests.forEach(({config, defaults, result}, index) => {
-  test(`defaultEslintConfigTests ${index}`, () => {
-    const merged = defaultEslintConfig(config, defaults)
-    expect(merged).toEqual(result)
-  })
-})
-
 getPrettierOptionsFromESLintRulesTests.forEach(
   ({rules, options, prettierOptions, fallbackPrettierOptions}, index) => {
     test(`getPrettierOptionsFromESLintRulesTests ${index}`, () => {
diff --git a/tests/fixtures/paths/node_modules/prettier/index.js b/tests/fixtures/paths/node_modules/prettier/index.js
index 69adb19..080f81a 100644
--- a/tests/fixtures/paths/node_modules/prettier/index.js
+++ b/tests/fixtures/paths/node_modules/prettier/index.js
@@ -1,6 +1,6 @@
 const mockPrettier = require('../../../../../src/__mocks__/prettier')
 
-module.exports = {format: mockMockFormat}
+module.exports = Object.assign({}, mockPrettier, {format: mockMockFormat})
 
 function mockMockFormat(...args) {
   try {
diff --git a/tests/fixtures/paths/package.json b/tests/fixtures/paths/package.json
index e4fa25b..9e26325 100644
--- a/tests/fixtures/paths/package.json
+++ b/tests/fixtures/paths/package.json
@@ -1,5 +1,9 @@
 {
   "eslintConfig": {
     "rules": {}
+  },
+  "prettier": {
+    "semi": false,
+    "singleQuote": true
   }
 }