Skip to content

Commit

Permalink
Add optional Prettier support (#271)
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg authored and sindresorhus committed Dec 31, 2017
1 parent 7c77af3 commit fd89175
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 1 deletion.
40 changes: 39 additions & 1 deletion lib/options-manager.js
Expand Up @@ -7,6 +7,7 @@ const multimatch = require('multimatch');
const pathExists = require('path-exists');
const pkgConf = require('pkg-conf');
const resolveFrom = require('resolve-from');
const prettier = require('prettier');

const getGitIgnoreFilter = require('./gitignore').getGitIgnoreFilter;

Expand Down Expand Up @@ -92,6 +93,25 @@ const mergeWithPkgConf = opts => {
return Object.assign({}, conf, opts);
};

const normalizeSpaces = opts => {
return typeof opts.space === 'number' ? opts.space : 2;
};

const mergeWithPrettierConf = opts => {
return mergeWith(
{},
{
singleQuote: true,
trailingComma: opts.esnext === false ? 'none' : 'es5',
bracketSpacing: false,
jsxBracketSameLine: false
},
prettier.resolveConfig.sync(opts.cwd || process.cwd()),
{tabWidth: normalizeSpaces(opts), useTabs: !opts.space, semi: opts.semicolon !== false},
mergeFn
);
};

// Define the shape of deep properties for mergeWith
const emptyOptions = () => ({
rules: {},
Expand All @@ -109,9 +129,9 @@ const buildConfig = opts => {
opts,
mergeFn
);
const spaces = normalizeSpaces(opts);

if (opts.space) {
const spaces = typeof opts.space === 'number' ? opts.space : 2;
config.rules.indent = ['error', spaces, {SwitchCase: 1}];

// Only apply if the user has the React plugin
Expand Down Expand Up @@ -179,6 +199,23 @@ const buildConfig = opts => {
config.baseConfig.extends = config.baseConfig.extends.concat(configs);
}

// If the user sets the `prettier` options then add the `prettier` plugin and config
if (opts.prettier) {
// The prettier plugin uses Prettier to format the code with `--fix`
config.plugins = config.plugins.concat('prettier');
// The prettier config overrides ESLint stylistic rules that are handled by Prettier
config.baseConfig.extends = config.baseConfig.extends.concat('prettier');
// The `prettier/prettier` rule reports errors if the code is not formatted in accordance to Prettier
config.rules['prettier/prettier'] = ['error', mergeWithPrettierConf(opts)];
// If the user has the React, Flowtype or Standard plugin, add the corresponding Prettier rule overrides
// See https://github.com/prettier/eslint-config-prettier for the list of plugins overrrides
for (const override of ['react', 'flowtype', 'standard']) {
if (opts.cwd && resolveFrom.silent(opts.cwd, `eslint-plugin-${override}`)) {
config.baseConfig.extends = config.baseConfig.extends.concat(`prettier/${override}`);
}
}
}

return config;
};

Expand Down Expand Up @@ -252,6 +289,7 @@ const preprocess = opts => {
module.exports.DEFAULT_IGNORE = DEFAULT_IGNORE;
module.exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
module.exports.mergeWithPkgConf = mergeWithPkgConf;
module.exports.mergeWithPrettierConf = mergeWithPrettierConf;
module.exports.normalizeOpts = normalizeOpts;
module.exports.buildConfig = buildConfig;
module.exports.findApplicableOverrides = findApplicableOverrides;
Expand Down
4 changes: 4 additions & 0 deletions main.js
Expand Up @@ -20,6 +20,7 @@ const cli = meow(`
--ignore Additional paths to ignore [Can be set multiple times]
--space Use space indent instead of tabs [Default: 2]
--no-semicolon Prevent use of semicolons
--prettier Conform to Prettier code style
--plugin Include third-party plugins [Can be set multiple times]
--extend Extend defaults with a custom config [Can be set multiple times]
--open Open files with issues in your editor
Expand Down Expand Up @@ -72,6 +73,9 @@ const cli = meow(`
// type: 'boolean',
// default: true
// },
prettier: {
type: 'boolean'
},
plugin: {
type: 'string'
},
Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -73,12 +73,14 @@
"arrify": "^1.0.0",
"debug": "^3.1.0",
"eslint": "^4.9.0",
"eslint-config-prettier": "^2.8.0",
"eslint-config-xo": "^0.19.0",
"eslint-formatter-pretty": "^1.0.0",
"eslint-plugin-ava": "^4.2.0",
"eslint-plugin-import": "^2.0.0",
"eslint-plugin-no-use-extend-native": "^0.3.2",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-prettier": "^2.3.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-unicorn": "^2.1.0",
"get-stdin": "^5.0.0",
Expand All @@ -92,6 +94,7 @@
"open-editor": "^1.0.1",
"path-exists": "^3.0.0",
"pkg-conf": "^2.0.0",
"prettier": "~1.9.2",
"resolve-cwd": "^2.0.0",
"resolve-from": "^4.0.0",
"slash": "^1.0.0",
Expand Down
9 changes: 9 additions & 0 deletions readme.md
Expand Up @@ -32,6 +32,7 @@ Uses [ESLint](http://eslint.org) underneath, so issues regarding rules should be
- Fix many issues automagically with `$ xo --fix`.
- Open all files with errors at the correct line in your editor with `$ xo --open`.
- Specify [indent](#indent) and [semicolon](#semicolon) preferences easily without messing with the rule config.
- Optionally use the [Prettier](https://github.com/prettier/prettier) code style.
- Great [editor plugins](#editor-plugins).


Expand Down Expand Up @@ -59,6 +60,7 @@ $ xo --help
--ignore Additional paths to ignore [Can be set multiple times]
--space Use space indent instead of tabs [Default: 2]
--no-semicolon Prevent use of semicolons
--prettier Conform to Prettier code style
--plugin Include third-party plugins [Can be set multiple times]
--extend Extend defaults with a custom config [Can be set multiple times]
--open Open files with issues in your editor
Expand Down Expand Up @@ -197,6 +199,13 @@ Default: `true` *(semicolons required)*

Set it to `false` to enforce no-semicolon style.

### prettier

Type: `boolean`<br>
Default: `false`

Format code with [Prettier](https://github.com/prettier/prettier). The [Prettier options](https://prettier.io/docs/en/options.html) will be read from the [Prettier config](https://prettier.io/docs/en/configuration.html)

### plugins

Type: `Array`
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/prettier/package.json
@@ -0,0 +1,11 @@
{
"prettier": {
"useTabs": false,
"semi": false,
"tabWidth": 4,
"bracketSpacing": false,
"jsxBracketSameLine": false,
"singleQuote": false,
"trailingComma": "all"
}
}
22 changes: 22 additions & 0 deletions test/lint-text.js
Expand Up @@ -109,6 +109,28 @@ test('extends support with `esnext` option', t => {
t.true(hasRule(results, 'react/jsx-no-undef'));
});

test('disable style rules when `prettier` option is enabled', t => {
const withoutPrettier = fn.lintText('(a) => {}\n', {}).results;
// `arrow-parens` is enabled
t.true(hasRule(withoutPrettier, 'arrow-parens'));
// `prettier/prettier` is disabled
t.false(hasRule(withoutPrettier, 'prettier/prettier'));

const withPrettier = fn.lintText('(a) => {}\n', {prettier: true}).results;
// `arrow-parens` is disabled by `eslint-config-prettier`
t.false(hasRule(withPrettier, 'arrow-parens'));
// `prettier/prettier` is enabled
t.true(hasRule(withPrettier, 'prettier/prettier'));
});

test('extends `react` support with `prettier` option', t => {
const results = fn.lintText('<Hello name={ firstname } />;\n', {extends: 'xo-react', prettier: true}).results;
// `react/jsx-curly-spacing` is disabled by `eslint-config-prettier`
t.false(hasRule(results, 'react/jsx-curly-spacing'));
// `prettier/prettier` is enabled
t.true(hasRule(results, 'prettier/prettier'));
});

test('always use the latest ECMAScript parser so esnext syntax won\'t throw in normal mode', t => {
const results = fn.lintText('async function foo() {}\n\nfoo();\n').results;
t.is(results[0].errorCount, 0);
Expand Down
80 changes: 80 additions & 0 deletions test/options-manager.js
Expand Up @@ -3,6 +3,7 @@ import test from 'ava';
import proxyquire from 'proxyquire';
import parentConfig from './fixtures/nested/package';
import childConfig from './fixtures/nested/child/package';
import prettierConfig from './fixtures/prettier/package';

process.chdir(__dirname);

Expand Down Expand Up @@ -72,6 +73,85 @@ test('buildConfig: semicolon', t => {
});
});

test('buildConfig: prettier: true', t => {
const config = manager.buildConfig({prettier: true, extends: ['xo-react']});

t.deepEqual(config.plugins, ['prettier']);
// Sets the `semi`, `useTabs` and `tabWidth` options in `prettier/prettier` based on the XO `space` and `semicolon` options
// Sets `singleQuote`, `trailingComma`, `bracketSpacing` and `jsxBracketSameLine` with XO defaults
t.deepEqual(config.rules, {
'prettier/prettier': ['error', {
useTabs: true,
bracketSpacing: false,
jsxBracketSameLine: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5'
}]});
// eslint-prettier-config must always be last
t.deepEqual(config.baseConfig.extends.slice(-1), ['prettier']);
});

test('buildConfig: prettier: true, semicolon: false', t => {
const config = manager.buildConfig({prettier: true, semicolon: false});

// Sets the `semi` options in `prettier/prettier` based on the XO `semicolon` option
t.deepEqual(config.rules['prettier/prettier'], ['error', {
useTabs: true,
bracketSpacing: false,
jsxBracketSameLine: false,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5'
}]);
});

test('buildConfig: prettier: true, space: 4', t => {
const config = manager.buildConfig({prettier: true, space: 4});

// Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options
t.deepEqual(config.rules['prettier/prettier'], ['error', {
useTabs: false,
bracketSpacing: false,
jsxBracketSameLine: false,
semi: true,
singleQuote: true,
tabWidth: 4,
trailingComma: 'es5'
}]);
});

test('buildConfig: prettier: true, esnext: false', t => {
const config = manager.buildConfig({prettier: true, esnext: false});

// Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options
t.deepEqual(config.rules['prettier/prettier'], ['error', {
useTabs: true,
bracketSpacing: false,
jsxBracketSameLine: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'none'
}]);
});

test('mergeWithPrettierConf: use `singleQuote`, `trailingComma`, `bracketSpacing` and `jsxBracketSameLine` from `prettier` config if defined', t => {
const cwd = path.resolve('fixtures', 'prettier');
const result = manager.mergeWithPrettierConf({cwd});
const expected = Object.assign({}, prettierConfig.prettier, {tabWidth: 2, useTabs: true, semi: true});
t.deepEqual(result, expected);
});

test('mergeWithPrettierConf: determine `tabWidth`, `useTabs`, `semi` from xo config', t => {
const cwd = path.resolve('fixtures', 'prettier');
const result = manager.mergeWithPrettierConf({cwd, space: 4, semicolon: false});
const expected = Object.assign({}, prettierConfig.prettier, {tabWidth: 4, useTabs: false, semi: false});
t.deepEqual(result, expected);
});

test('buildConfig: rules', t => {
const rules = {'object-curly-spacing': ['error', 'always']};
const config = manager.buildConfig({rules});
Expand Down

0 comments on commit fd89175

Please sign in to comment.