From fa7437d809feeeb7adf83139b16d5b52abbadd3e Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 24 Jan 2022 14:34:50 -0700 Subject: [PATCH 1/7] feat: :sparkles: Support ESLint 8 These are breaking changes as the ESLint API has changed with version 8 --- package.json | 12 ++--- src/__mocks__/eslint.js | 47 +++++++++-------- src/__tests__/index.js | 110 ++++++++++++++++++++-------------------- src/index.js | 53 ++++++++++++------- src/utils.js | 48 +++++++----------- 5 files changed, 139 insertions(+), 131 deletions(-) diff --git a/package.json b/package.json index 86f3d826..acbb0822 100644 --- a/package.json +++ b/package.json @@ -18,18 +18,18 @@ ], "license": "MIT", "dependencies": { - "@typescript-eslint/parser": "^3.0.0", + "@typescript-eslint/parser": "^5.10.0", "common-tags": "^1.4.0", "dlv": "^1.1.0", - "eslint": "^7.9.0", + "eslint": "^8.7.0", "indent-string": "^4.0.0", "lodash.merge": "^4.6.0", "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^2.0.0", + "prettier": "^2.5.1", "pretty-format": "^23.0.1", "require-relative": "^0.8.7", - "typescript": "^3.9.3", - "vue-eslint-parser": "~7.1.0" + "typescript": "^4.5.4", + "vue-eslint-parser": "^8.0.1" }, "devDependencies": { "@babel/cli": "^7.4.4", @@ -40,7 +40,7 @@ "all-contributors-cli": "^6.7.0", "babel-jest": "^25.0.0", "chalk": "^2.1.0", - "eslint-config-kentcdodds": "~16.0.1", + "eslint-config-kentcdodds": "^20.0.1", "husky": "^2.4.1", "jest": "^25.0.0", "jest-cli": "^25.0.0", diff --git a/src/__mocks__/eslint.js b/src/__mocks__/eslint.js index 10369ec7..c48cc8f3 100644 --- a/src/__mocks__/eslint.js +++ b/src/__mocks__/eslint.js @@ -2,35 +2,35 @@ // search around the file system for stuff const eslint = jest.requireActual('eslint'); -const { CLIEngine } = eslint; +const { ESLint } = eslint; -const mockGetConfigForFileSpy = jest.fn(mockGetConfigForFile); -mockGetConfigForFileSpy.overrides = {}; -const mockExecuteOnTextSpy = jest.fn(mockExecuteOnText); +const mockCalculateConfigForFileSpy = jest.fn(mockCalculateConfigForFile); +mockCalculateConfigForFileSpy.overrides = {}; +const mockLintTextSpy = jest.fn(mockLintText); module.exports = Object.assign(eslint, { - CLIEngine: jest.fn(MockCLIEngine), + ESLint: jest.fn(MockESLint), mock: { - getConfigForFile: mockGetConfigForFileSpy, - executeOnText: mockExecuteOnTextSpy + calculateConfigForFile: mockCalculateConfigForFileSpy, + lintText: mockLintTextSpy } }); -function MockCLIEngine(...args) { +function MockESLint(...args) { global.__PRETTIER_ESLINT_TEST_STATE__.eslintPath = __filename; - const cliEngine = new CLIEngine(...args); - cliEngine.getConfigForFile = mockGetConfigForFileSpy; - cliEngine._originalExecuteOnText = cliEngine.executeOnText; - cliEngine.executeOnText = mockExecuteOnTextSpy; - return cliEngine; + const eslintInstance = new ESLint(...args); + eslintInstance.calculateConfigForFile = mockCalculateConfigForFileSpy; + eslintInstance._originalLintText = eslintInstance.lintText; + eslintInstance.lintText = mockLintTextSpy; + return eslintInstance; } -MockCLIEngine.prototype = Object.create(CLIEngine.prototype); +MockESLint.prototype = Object.create(ESLint.prototype); // eslint-disable-next-line complexity -function mockGetConfigForFile(filePath) { - if (mockGetConfigForFileSpy.throwError) { - throw mockGetConfigForFileSpy.throwError; +function mockCalculateConfigForFile(filePath) { + if (mockCalculateConfigForFileSpy.throwError) { + throw mockCalculateConfigForFileSpy.throwError; } if (!filePath) { return { @@ -39,7 +39,7 @@ function mockGetConfigForFile(filePath) { } if (filePath.includes('default-config')) { return { - rules: { + rules: { semi: [2, 'never'], 'max-len': [2, 120, 2], indent: [2, 2, { SwitchCase: 1 }], @@ -59,8 +59,7 @@ function mockGetConfigForFile(filePath) { } ], 'arrow-parens': [2, 'as-needed'] - } - }; + }}; } else if (filePath.includes('fixtures/paths')) { return { rules: {} }; } else { @@ -71,10 +70,10 @@ function mockGetConfigForFile(filePath) { } } -function mockExecuteOnText(...args) { +function mockLintText(...args) { /* eslint babel/no-invalid-this:0 */ - if (mockExecuteOnTextSpy.throwError) { - throw mockExecuteOnTextSpy.throwError; + if (mockLintTextSpy.throwError) { + throw mockLintTextSpy.throwError; } - return this._originalExecuteOnText(...args); + return this._originalLintText(...args); } diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 1098bb25..58bd610f 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -63,7 +63,6 @@ const tests = [ input: { text: 'const { foo } = bar;', eslintConfig: { - foo: true, // Won't be overridden parserOptions: { ecmaVersion: 7 @@ -226,8 +225,8 @@ const tests = [ ]; beforeEach(() => { - eslintMock.mock.executeOnText.mockClear(); - eslintMock.mock.getConfigForFile.mockClear(); + eslintMock.mock.lintText.mockClear(); + eslintMock.mock.calculateConfigForFile.mockClear(); prettierMock.format.mockClear(); prettierMock.resolveConfig.sync.mockClear(); fsMock.readFileSync.mockClear(); @@ -240,55 +239,55 @@ 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', () => { - const { executeOnText } = eslintMock.mock; +test('failure to fix with eslint throws and logs an error', async () => { + const { lintText } = eslintMock.mock; const error = new Error('Something happened'); - executeOnText.throwError = error; + lintText.throwError = error; - expect(() => format({ text: '' })).toThrowError(error); + await expect(() => format({ text: '' })).rejects.toThrowError(error); expect(logger.error).toHaveBeenCalledTimes(1); - executeOnText.throwError = null; + lintText.throwError = null; }); -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`, () => { +test(`when prettier throws, log to logger.error and throw the error`, async () => { const error = new Error('something bad happened'); prettierMock.format.throwError = error; - expect(() => format({ text: '' })).toThrowError(error); + await expect(() => format({ text: '' })).rejects.toThrowError(error); expect(logger.error).toHaveBeenCalledTimes(1); prettierMock.format.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'); - format({ text: '', eslintPath }); - expect(eslintMock.mock.executeOnText).toHaveBeenCalledTimes(1); + await format({ text: '', eslintPath }); + expect(eslintMock.mock.lintText).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-existent-eslint-module' ); - expect(() => format({ text: '', eslintPath })).toThrowError( + await expect(() => format({ text: '', eslintPath })).rejects.toThrowError( /non-existent-eslint-module/ ); expect(logger.error).toHaveBeenCalledTimes(1); @@ -306,13 +305,13 @@ test('can accept a path to a prettier module and uses that instead.', () => { 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-existent-prettier-module' ); - expect(() => format({ text: '', prettierPath })).toThrowError( + await expect(() => format({ text: '', prettierPath })).rejects.toThrowError( /non-existent-prettier-module/ ); expect(logger.error).toHaveBeenCalledTimes(1); @@ -336,9 +335,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.js'; - 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 @@ -348,31 +347,31 @@ 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 readFileSyncMockSpy = jest.spyOn(fsMock, 'readFileSync'); const filePath = '/blah-blah/some-file.js'; - format({ filePath }); + await format({ filePath }); expect(readFileSyncMockSpy).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( + await expect(() => format({ filePath: '/some-path.js' })).rejects.toThrowError( /some error/ ); expect(logger.error).toHaveBeenCalledTimes(1); fsMock.readFileSync = originalMock; }); -test('calls prettier.resolveConfig.sync with the file path', () => { +test('calls prettier.resolveConfig.sync with the file path', async () => { const filePath = require.resolve('../../tests/fixtures/paths/foo.js'); - format({ + await format({ filePath, text: defaultInputText(), eslintConfig: getESLintConfigWithDefaultRules() @@ -399,12 +398,12 @@ test('does not raise an error if prettier.resolveConfig.sync is not defined', () prettierMock.resolveConfig.sync = originalPrettierMockResolveConfigSync; }); -test('does not raise an error if prettier.resolveConfig is not defined', () => { +test('does not raise an error if prettier.resolveConfig is not defined', async () => { const filePath = require.resolve('../../tests/fixtures/paths/foo.js'); const originalPrettierMockResolveConfig = prettierMock.resolveConfig; prettierMock.resolveConfig = undefined; - function callingFormat() { + async function callingFormat() { return format({ filePath, text: defaultInputText(), @@ -412,42 +411,43 @@ test('does not raise an error if prettier.resolveConfig is not defined', () => { }); } - expect(callingFormat).not.toThrowError(); + await expect(callingFormat).not.toThrowError(); prettierMock.resolveConfig = originalPrettierMockResolveConfig; }); -test('logs if there is a problem making the CLIEngine', () => { +test('logs if there is a problem making the CLIEngine', async () => { const error = new Error('fake error'); - eslintMock.CLIEngine.mockImplementation(() => { + eslintMock.ESLint.mockImplementation(() => { throw error; }); - expect(() => format({ text: '' })).toThrowError(error); - eslintMock.CLIEngine.mockReset(); + await expect(() => format({ text: '' })).rejects.toThrowError(error); + eslintMock.ESLint.mockReset(); expect(logger.error).toHaveBeenCalledTimes(1); }); function getESLintConfigWithDefaultRules(overrides) { return { - parserOptions: { ecmaVersion: 7 }, - rules: { - semi: [2, 'never'], - 'max-len': [2, 120, 2], - indent: [2, 2, { SwitchCase: 1 }], - quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], - 'comma-dangle': [ - 2, - { - arrays: 'always-multiline', - objects: 'always-multiline', - imports: 'always-multiline', - exports: 'always-multiline', - functions: 'always-multiline' - } - ], - 'arrow-parens': [2, 'as-needed'], - ...overrides - } + + parserOptions: { ecmaVersion: 7 }, + rules: { + semi: [2, 'never'], + 'max-len': [2, 120, 2], + indent: [2, 2, { SwitchCase: 1 }], + quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], + 'comma-dangle': [ + 2, + { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'always-multiline' + } + ], + 'arrow-parens': [2, 'as-needed'], + ...overrides + } }; } diff --git a/src/index.js b/src/index.js index 60d53eb3..24f03958 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ import indentString from 'indent-string'; import getLogger from 'loglevel-colored-level-prefix'; import merge from 'lodash.merge'; import { - getESLintCLIEngine, + getESLint, getOptionsForFormatting, requireModule } from './utils'; @@ -44,7 +44,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)); @@ -57,19 +57,13 @@ function format(options) { prettierLast, fallbackPrettierOptions } = options; - + const eslintConfig = merge( {}, options.eslintConfig, getESLintConfig(filePath, eslintPath) ); - if (typeof eslintConfig.globals === 'object') { - eslintConfig.globals = Object.entries(eslintConfig.globals).map( - ([key, value]) => `${key}:${value}` - ); - } - const prettierOptions = merge( {}, filePath && { filepath: filePath }, @@ -130,8 +124,8 @@ function format(options) { formattingOptions.eslint.parser = formattingOptions.eslint.parser || require.resolve('vue-eslint-parser'); } - - const eslintFix = createEslintFix(formattingOptions.eslint, eslintPath); + + const eslintFix = await createEslintFix(formattingOptions.eslint, eslintPath); if (prettierLast) { return prettify(eslintFix(text, filePath)); @@ -170,18 +164,43 @@ function createPrettify(formatOptions, prettierPath) { } function createEslintFix(eslintConfig, eslintPath) { - return function eslintFix(text, filePath) { - const cliEngine = getESLintCLIEngine(eslintPath, eslintConfig); + return async function eslintFix(text, filePath) { + + if (Array.isArray(eslintConfig.globals)) { + const tempGlobals = {}; + eslintConfig.globals.forEach(g => { + const [key,value] = g.split(':'); + tempGlobals[key] = value; + }); + eslintConfig.globals = tempGlobals; + } + + eslintConfig.overrideConfig = { + rules: eslintConfig.rules, + parser: eslintConfig.parser, + globals: eslintConfig.globals, + parserOptions: eslintConfig.parserOptions, + ignorePatterns: eslintConfig.ignorePattern, + ...eslintConfig.overrideConfig, + }; + + delete eslintConfig.rules; + delete eslintConfig.parser; + delete eslintConfig.parserOptions; + delete eslintConfig.globals; + delete eslintConfig.ignorePattern; + + const eslint = getESLint(eslintPath, eslintConfig); try { logger.trace(`calling cliEngine.executeOnText with the text`); - const report = cliEngine.executeOnText(text, filePath, true); + const report = await eslint.lintText(text, { filePath, warnIgnored: true }); logger.trace( `executeOnText returned the following report:`, prettyFormat(report) ); // default the output to text because if there's nothing // to fix, eslint doesn't provide `output` - const [{ output = text }] = report.results; + const [{ output = text }] = await report; logger.trace('eslint --fix: output === input', output === text); // NOTE: We're ignoring linting errors/warnings here and // defaulting to the given text if there are any @@ -233,10 +252,10 @@ function getESLintConfig(filePath, eslintPath) { "${filePath || process.cwd()}" ` ); - const cliEngine = getESLintCLIEngine(eslintPath, eslintOptions); + const eslint = getESLint(eslintPath, eslintOptions); try { logger.debug(`getting eslint config for file at "${filePath}"`); - const config = cliEngine.getConfigForFile(filePath); + const config = eslint.calculateConfigForFile(filePath); logger.trace( `eslint config for "${filePath}" received`, prettyFormat(config) diff --git a/src/utils.js b/src/utils.js index acfa3d51..4de293f5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,8 @@ import { oneLine } from 'common-tags'; import delve from 'dlv'; import getLogger from 'loglevel-colored-level-prefix'; +import merge from 'lodash.merge'; +import { Linter } from 'eslint'; const logger = getLogger({ prefix: 'prettier-eslint' }); const RULE_DISABLED = {}; @@ -57,15 +59,14 @@ const OPTION_GETTERS = { }; /* eslint import/prefer-default-export:0 */ -export { getESLintCLIEngine, getOptionsForFormatting, requireModule }; +export { getESLint, getOptionsForFormatting, requireModule }; function getOptionsForFormatting( eslintConfig, prettierOptions = {}, fallbackPrettierOptions = {}, - eslintPath ) { - const eslint = getRelevantESLintConfig(eslintConfig, eslintPath); + const eslint = getRelevantESLintConfig(eslintConfig); const prettier = getPrettierOptionsFromESLintRules( eslintConfig, prettierOptions, @@ -74,34 +75,23 @@ function getOptionsForFormatting( return { eslint, prettier }; } -function getRelevantESLintConfig(eslintConfig, eslintPath) { - const cliEngine = getESLintCLIEngine(eslintPath); - // TODO: Actually test this branch - // istanbul ignore next - const loadedRules = - (cliEngine.getRules && cliEngine.getRules()) || - // XXX: Fallback list of unfixable rules, when using and old version of eslint - new Map([['global-require', { meta: {} }], ['no-with', { meta: {} }]]); - - const { rules } = eslintConfig; - +function getRelevantESLintConfig(eslintConfig) { + const linter = new Linter(); + const rules = linter.getRules(); logger.debug('turning off unfixable rules'); - const relevantRules = Object.entries(rules).reduce( - (rulesAccumulator, [name, rule]) => { - if (loadedRules.has(name)) { + const relevantRules = {}; + + rules.forEach((rule, name) => { const { meta: { fixable } - } = loadedRules.get(name); - + } = rule; + if (!fixable) { - logger.trace('turing off rule:', JSON.stringify({ [name]: rule })); + logger.trace('turning off rule:', JSON.stringify({ [name]: rule })); rule = ['off']; + relevantRules[name] = rule; } - } - - rulesAccumulator[name] = rule; - return rulesAccumulator; }, {} ); @@ -114,7 +104,7 @@ function getRelevantESLintConfig(eslintConfig, eslintPath) { }, ...eslintConfig, // overrides - rules: relevantRules, + rules: { ...eslintConfig.rules, ...relevantRules }, fix: true, globals: [] }; @@ -344,7 +334,7 @@ function extractRuleValue(objPath, name, value) { function getRuleValue(rules, name, objPath) { const ruleConfig = rules[name]; - + if (Array.isArray(ruleConfig)) { const [ruleSetting, value] = ruleConfig; @@ -417,10 +407,10 @@ function requireModule(modulePath, name) { } } -function getESLintCLIEngine(eslintPath, eslintOptions) { - const { CLIEngine } = requireModule(eslintPath, 'eslint'); +function getESLint(eslintPath, eslintOptions) { + const { ESLint } = requireModule(eslintPath, 'eslint'); try { - return new CLIEngine(eslintOptions); + return new ESLint(eslintOptions); } catch (error) { logger.error(`There was trouble creating the ESLint CLIEngine.`); throw error; From 009b377206fba64aad069dda4c3e0580368c79ae Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 31 Jan 2022 17:03:07 -0700 Subject: [PATCH 2/7] Fix tests --- .eslintrc.js | 3 ++- src/__mocks__/eslint.js | 2 +- src/__tests__/index.js | 9 +++++---- src/index.js | 3 ++- src/utils.js | 1 - tests/fixtures/paths/node_modules/eslint/index.js | 6 +++--- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6f6c05b7..0748919c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,8 @@ const config = { named: "never", asyncArrow: "always" } - ] + ], + "import/no-import-module-exports": "off" } }; diff --git a/src/__mocks__/eslint.js b/src/__mocks__/eslint.js index c48cc8f3..7276c110 100644 --- a/src/__mocks__/eslint.js +++ b/src/__mocks__/eslint.js @@ -71,7 +71,7 @@ function mockCalculateConfigForFile(filePath) { } function mockLintText(...args) { - /* eslint babel/no-invalid-this:0 */ + /* eslint no-invalid-this:0 */ if (mockLintTextSpy.throwError) { throw mockLintTextSpy.throwError; } diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 58bd610f..b7b34117 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -299,9 +299,10 @@ test('fails with an error if the eslint module cannot be resolved.', async () => 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'); - format({ text: '', prettierPath }); + await format({ text: '', prettierPath}); + expect(prettierMock.format).toHaveBeenCalledTimes(1); }); @@ -321,9 +322,9 @@ test('fails with an error if the prettier module cannot be resolved.', async () 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' diff --git a/src/index.js b/src/index.js index 24f03958..e553b814 100644 --- a/src/index.js +++ b/src/index.js @@ -128,7 +128,8 @@ async function format(options) { const eslintFix = await createEslintFix(formattingOptions.eslint, eslintPath); if (prettierLast) { - return prettify(eslintFix(text, filePath)); + const eslintFixed = await eslintFix(text, filePath); + return prettify(eslintFixed); } return eslintFix(prettify(text), filePath); } diff --git a/src/utils.js b/src/utils.js index 4de293f5..0670c3eb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,7 +2,6 @@ import { oneLine } from 'common-tags'; import delve from 'dlv'; import getLogger from 'loglevel-colored-level-prefix'; -import merge from 'lodash.merge'; import { Linter } from 'eslint'; const logger = getLogger({ prefix: 'prettier-eslint' }); diff --git a/tests/fixtures/paths/node_modules/eslint/index.js b/tests/fixtures/paths/node_modules/eslint/index.js index 1544d74e..592f4f48 100644 --- a/tests/fixtures/paths/node_modules/eslint/index.js +++ b/tests/fixtures/paths/node_modules/eslint/index.js @@ -1,11 +1,11 @@ const eslintMock = require('../../../../../src/__mocks__/eslint') module.exports = Object.assign({}, eslintMock, { - CLIEngine: MockMockCLIEngine + ESLint: MockMockESLint }) -function MockMockCLIEngine(...args) { +function MockMockESLint(...args) { try { - return eslintMock.CLIEngine.apply(this, args) + return eslintMock.ESLint.apply(this, args) } finally { global.__PRETTIER_ESLINT_TEST_STATE__.eslintPath = __filename } From 4f4718900b49ea4227a2bf343939f2e872b0f158 Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 31 Jan 2022 18:12:52 -0700 Subject: [PATCH 3/7] Fix test coverage --- src/__tests__/index.js | 8 ++++++++ src/__tests__/utils.js | 2 +- src/utils.js | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/__tests__/index.js b/src/__tests__/index.js index b7b34117..68a9065b 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -139,6 +139,14 @@ const tests = [ }, output: 'var [foo, { bar }] = window.APP;' }, + { + title: 'accepts config globals as array', + input: { + text: defaultInputText(), + eslintConfig: { globals: ['window:writable']} + }, + output: noopOutput() + }, { title: 'CSS example', input: { diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index 0a84398a..5ec871bf 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -309,7 +309,7 @@ test('Turn off unfixable rules', () => { quotes: ['error', 'double'] }, fix: true, - globals: [], + globals: {}, useEslintrc: false }); }); diff --git a/src/utils.js b/src/utils.js index 0670c3eb..7609520e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -105,7 +105,7 @@ function getRelevantESLintConfig(eslintConfig) { // overrides rules: { ...eslintConfig.rules, ...relevantRules }, fix: true, - globals: [] + globals: eslintConfig.globals || {} }; } From 745e42e34d888868391dc1d0cd871c769486d3f5 Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 31 Jan 2022 18:19:03 -0700 Subject: [PATCH 4/7] Add to contributors --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d44891f8..ee2f3c84 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -301,6 +301,15 @@ "contributions": [ "maintenance" ] + }, + { + "login": "idahogurl", + "name": "Rebecca Vest", + "avatar_url": "https://avatars.githubusercontent.com/u/10620169?v=4", + "profile": "https://campcode.dev/", + "contributions": [ + "code" + ] } ], "repoType": "github", diff --git a/README.md b/README.md index 87e0135e..f6533150 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ const options = { }, }; -const formatted = format(options); +const formatted = await format(options); // notice no semicolon in the formatted text formatted; // const { foo } = bar @@ -304,6 +304,7 @@ Thanks goes to these people ([emoji key][emojis]):
Igor

🚧 +
Rebecca Vest

💻 From c71310f194e7a1bb146a431e9d71527542a0ae59 Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 31 Jan 2022 19:54:54 -0700 Subject: [PATCH 5/7] Prettier --- .eslintrc.js | 18 ++++++------- .prettierrc.js | 4 +++ src/__mocks__/eslint.js | 5 ++-- src/__tests__/index.js | 59 +++++++++++++++++++---------------------- src/__tests__/utils.js | 2 +- src/index.js | 28 +++++++++---------- src/utils.js | 32 +++++++++++----------- 7 files changed, 73 insertions(+), 75 deletions(-) create mode 100644 .prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js index 0748919c..9c6d945d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,17 +1,17 @@ const config = { - extends: ["kentcdodds", "kentcdodds/jest"], + extends: ['kentcdodds', 'kentcdodds/jest'], rules: { - "valid-jsdoc": "off", - "max-len": "off", - "space-before-function-paren": [ - "error", + 'valid-jsdoc': 'off', + 'max-len': 'off', + 'space-before-function-paren': [ + 'error', { - anonymous: "never", - named: "never", - asyncArrow: "always" + anonymous: 'never', + named: 'never', + asyncArrow: 'always' } ], - "import/no-import-module-exports": "off" + 'import/no-import-module-exports': 'off' } }; diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..e4d181d9 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,4 @@ +module.exports = { + arrowParens: 'avoid', + singleQuote: true +} \ No newline at end of file diff --git a/src/__mocks__/eslint.js b/src/__mocks__/eslint.js index 7276c110..dced55a5 100644 --- a/src/__mocks__/eslint.js +++ b/src/__mocks__/eslint.js @@ -39,7 +39,7 @@ function mockCalculateConfigForFile(filePath) { } if (filePath.includes('default-config')) { return { - rules: { + rules: { semi: [2, 'never'], 'max-len': [2, 120, 2], indent: [2, 2, { SwitchCase: 1 }], @@ -59,7 +59,8 @@ function mockCalculateConfigForFile(filePath) { } ], 'arrow-parens': [2, 'as-needed'] - }}; + } + }; } else if (filePath.includes('fixtures/paths')) { return { rules: {} }; } else { diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 68a9065b..3ef58b07 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -143,7 +143,7 @@ const tests = [ title: 'accepts config globals as array', input: { text: defaultInputText(), - eslintConfig: { globals: ['window:writable']} + eslintConfig: { globals: ['window:writable'] } }, output: noopOutput() }, @@ -187,8 +187,7 @@ const tests = [ 'space-before-function-paren': [2, 'always'] } }, - text: - '\n\n', + text: '\n\n', filePath: path.resolve('./test.vue') }, output: @@ -274,7 +273,7 @@ test('logLevel is used to configure the logger', async () => { expect(logger.setLevel).toHaveBeenCalledWith('silent'); }); -test(`when prettier throws, log to logger.error and throw the error`, async () => { +test('when prettier throws, log to logger.error and throw the error', async () => { const error = new Error('something bad happened'); prettierMock.format.throwError = error; @@ -309,8 +308,8 @@ test('fails with an error if the eslint module cannot be resolved.', async () => test('can accept a path to a prettier module and uses that instead.', async () => { const prettierPath = path.join(__dirname, '../__mocks__/prettier'); - await format({ text: '', prettierPath}); - + await format({ text: '', prettierPath }); + expect(prettierMock.format).toHaveBeenCalledTimes(1); }); @@ -361,9 +360,8 @@ test('reads text from fs if filePath is provided but not text', async () => { const filePath = '/blah-blah/some-file.js'; await format({ filePath }); - - expect(readFileSyncMockSpy).toHaveBeenCalledWith(filePath, 'utf8'); + expect(readFileSyncMockSpy).toHaveBeenCalledWith(filePath, 'utf8'); }); test('logs error if it cannot read the file from the filePath', async () => { @@ -371,9 +369,9 @@ test('logs error if it cannot read the file from the filePath', async () => { fsMock.readFileSync = jest.fn(() => { throw new Error('some error'); }); - await expect(() => format({ filePath: '/some-path.js' })).rejects.toThrowError( - /some error/ - ); + await expect(() => + format({ filePath: '/some-path.js' }) + ).rejects.toThrowError(/some error/); expect(logger.error).toHaveBeenCalledTimes(1); fsMock.readFileSync = originalMock; }); @@ -437,26 +435,25 @@ test('logs if there is a problem making the CLIEngine', async () => { function getESLintConfigWithDefaultRules(overrides) { return { - - parserOptions: { ecmaVersion: 7 }, - rules: { - semi: [2, 'never'], - 'max-len': [2, 120, 2], - indent: [2, 2, { SwitchCase: 1 }], - quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], - 'comma-dangle': [ - 2, - { - arrays: 'always-multiline', - objects: 'always-multiline', - imports: 'always-multiline', - exports: 'always-multiline', - functions: 'always-multiline' - } - ], - 'arrow-parens': [2, 'as-needed'], - ...overrides - } + parserOptions: { ecmaVersion: 7 }, + rules: { + semi: [2, 'never'], + 'max-len': [2, 120, 2], + indent: [2, 2, { SwitchCase: 1 }], + quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], + 'comma-dangle': [ + 2, + { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'always-multiline' + } + ], + 'arrow-parens': [2, 'as-needed'], + ...overrides + } }; } diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index 5ec871bf..458bc0e1 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -211,7 +211,7 @@ test('if prettierOptions are provided, those are preferred', () => { }); // eslint-disable-next-line max-len -test(`if fallbacks are provided, those are preferred over disabled eslint rules`, () => { +test('if fallbacks are provided, those are preferred over disabled eslint rules', () => { const { prettier } = getOptionsForFormatting( { rules: { diff --git a/src/index.js b/src/index.js index e553b814..e730b0ee 100644 --- a/src/index.js +++ b/src/index.js @@ -9,11 +9,7 @@ import { oneLine, stripIndent } from 'common-tags'; import indentString from 'indent-string'; import getLogger from 'loglevel-colored-level-prefix'; import merge from 'lodash.merge'; -import { - getESLint, - getOptionsForFormatting, - requireModule -} from './utils'; +import { getESLint, getOptionsForFormatting, requireModule } from './utils'; const logger = getLogger({ prefix: 'prettier-eslint' }); @@ -57,7 +53,7 @@ async function format(options) { prettierLast, fallbackPrettierOptions } = options; - + const eslintConfig = merge( {}, options.eslintConfig, @@ -124,7 +120,7 @@ async function format(options) { formattingOptions.eslint.parser = formattingOptions.eslint.parser || require.resolve('vue-eslint-parser'); } - + const eslintFix = await createEslintFix(formattingOptions.eslint, eslintPath); if (prettierLast) { @@ -146,7 +142,7 @@ function createPrettify(formatOptions, prettierPath) { ); const prettier = requireModule(prettierPath, 'prettier'); try { - logger.trace(`calling prettier.format with the text and prettierOptions`); + logger.trace('calling prettier.format with the text and prettierOptions'); const output = prettier.format(text, formatOptions); logger.trace('prettier: output === input', output === text); logger.trace( @@ -166,23 +162,22 @@ function createPrettify(formatOptions, prettierPath) { function createEslintFix(eslintConfig, eslintPath) { return async function eslintFix(text, filePath) { - if (Array.isArray(eslintConfig.globals)) { const tempGlobals = {}; eslintConfig.globals.forEach(g => { - const [key,value] = g.split(':'); + const [key, value] = g.split(':'); tempGlobals[key] = value; }); eslintConfig.globals = tempGlobals; } - eslintConfig.overrideConfig = { + eslintConfig.overrideConfig = { rules: eslintConfig.rules, parser: eslintConfig.parser, globals: eslintConfig.globals, parserOptions: eslintConfig.parserOptions, ignorePatterns: eslintConfig.ignorePattern, - ...eslintConfig.overrideConfig, + ...eslintConfig.overrideConfig }; delete eslintConfig.rules; @@ -193,10 +188,13 @@ function createEslintFix(eslintConfig, eslintPath) { const eslint = getESLint(eslintPath, eslintConfig); try { - logger.trace(`calling cliEngine.executeOnText with the text`); - const report = await eslint.lintText(text, { filePath, warnIgnored: true }); + logger.trace('calling cliEngine.executeOnText with the text'); + const report = await eslint.lintText(text, { + filePath, + warnIgnored: true + }); logger.trace( - `executeOnText returned the following report:`, + 'executeOnText returned the following report:', prettyFormat(report) ); // default the output to text because if there's nothing diff --git a/src/utils.js b/src/utils.js index 7609520e..3ebd1ce5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -63,7 +63,7 @@ export { getESLint, getOptionsForFormatting, requireModule }; function getOptionsForFormatting( eslintConfig, prettierOptions = {}, - fallbackPrettierOptions = {}, + fallbackPrettierOptions = {} ) { const eslint = getRelevantESLintConfig(eslintConfig); const prettier = getPrettierOptionsFromESLintRules( @@ -79,21 +79,19 @@ function getRelevantESLintConfig(eslintConfig) { const rules = linter.getRules(); logger.debug('turning off unfixable rules'); - const relevantRules = {}; - + const relevantRules = {}; + rules.forEach((rule, name) => { - const { - meta: { fixable } - } = rule; - - if (!fixable) { - logger.trace('turning off rule:', JSON.stringify({ [name]: rule })); - rule = ['off']; - relevantRules[name] = rule; - } - }, - {} - ); + const { + meta: { fixable } + } = rule; + + if (!fixable) { + logger.trace('turning off rule:', JSON.stringify({ [name]: rule })); + rule = ['off']; + relevantRules[name] = rule; + } + }, {}); return { // defaults @@ -333,7 +331,7 @@ function extractRuleValue(objPath, name, value) { function getRuleValue(rules, name, objPath) { const ruleConfig = rules[name]; - + if (Array.isArray(ruleConfig)) { const [ruleSetting, value] = ruleConfig; @@ -411,7 +409,7 @@ function getESLint(eslintPath, eslintOptions) { try { return new ESLint(eslintOptions); } catch (error) { - logger.error(`There was trouble creating the ESLint CLIEngine.`); + logger.error('There was trouble creating the ESLint CLIEngine.'); throw error; } } From f3ef24e28b29f7f2a79bff440c67eabb4cbf92db Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 18 Apr 2022 16:41:49 -0600 Subject: [PATCH 6/7] feat: :arrow_up: Upgrade to ESLint 8 BREAKING CHANGES: The `format` function is now asynchronous. Closes #656 From bed24b6ec62bc07489e39bcc5af0d87144f02b57 Mon Sep 17 00:00:00 2001 From: Rebecca Vest Date: Mon, 18 Apr 2022 16:44:50 -0600 Subject: [PATCH 7/7] Run CI on pull requests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cc8a935..929c5212 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: [push] +on: [push, pull_request] jobs: ci: