Skip to content

Commit c8058c9

Browse files
committed
Enforce rules based on "engines" field in package.json
1 parent fd89175 commit c8058c9

File tree

7 files changed

+218
-5
lines changed

7 files changed

+218
-5
lines changed

lib/options-manager.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const pathExists = require('path-exists');
88
const pkgConf = require('pkg-conf');
99
const resolveFrom = require('resolve-from');
1010
const prettier = require('prettier');
11+
const semver = require('semver');
1112

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

@@ -44,6 +45,43 @@ const DEFAULT_CONFIG = {
4445
}
4546
};
4647

48+
/**
49+
* Define the rules that are enabled only for specific version of Node, based on `engines.node` in package.json or the `node-engine` option.
50+
*
51+
* The keys ae rule names and the values are an Object with a valid semver (`4.0.0` is valid `4` is not) as keys and the rule configuration as values.
52+
* Each entry define the rule configuration and the minimum Node version for which to set it.
53+
* The entry with the highest version that is compliant with the `engines.node`/`node-engine` range will be used.
54+
*
55+
* @type {Object}
56+
*
57+
* @example
58+
* ```javascript
59+
* {
60+
* 'plugin/rule': {
61+
* '6.0.0': ['error', {prop: 'node-6-conf'}],
62+
* '8.0.0': ['error', {prop: 'node-8-conf'}]
63+
* }
64+
* }
65+
*```
66+
* With `engines.node` set to `>=4` the rule `plugin/rule` will not be used.
67+
* With `engines.node` set to `>=6` the rule `plugin/rule` will be used with the config `{prop: 'node-6-conf'}`.
68+
* With `engines.node` set to `>=8` the rule `plugin/rule` will be used with the config `{prop: 'node-8-conf'}`.
69+
*/
70+
const ENGINE_RULES = {
71+
'promise/prefer-await-to-then': {
72+
'8.0.0': 'error'
73+
},
74+
'prefer-rest-params': {
75+
'6.0.0': 'error'
76+
},
77+
'prefer-spread': {
78+
'5.0.0': 'error'
79+
},
80+
'prefer-destructuring': {
81+
'6.0.0': ['error', {array: true, object: true}, {enforceForRenamedProperties: true}]
82+
}
83+
};
84+
4785
// Keep the same behaviour in mergeWith as deepAssign
4886
const mergeFn = (prev, val) => {
4987
if (Array.isArray(prev) && Array.isArray(val)) {
@@ -90,7 +128,8 @@ const mergeWithPkgConf = opts => {
90128
opts = Object.assign({cwd: process.cwd()}, opts);
91129
opts.cwd = path.resolve(opts.cwd);
92130
const conf = pkgConf.sync('xo', {cwd: opts.cwd, skipOnFalse: true});
93-
return Object.assign({}, conf, opts);
131+
const engines = pkgConf.sync('engines', {cwd: opts.cwd});
132+
return Object.assign({}, conf, {engines}, opts);
94133
};
95134

96135
const normalizeSpaces = opts => {
@@ -131,6 +170,17 @@ const buildConfig = opts => {
131170
);
132171
const spaces = normalizeSpaces(opts);
133172

173+
if (opts.engines && opts.engines.node && semver.validRange(opts.engines.node)) {
174+
for (const rule of Object.keys(ENGINE_RULES)) {
175+
// Use the rule value for the highest version that is lower or equal to the oldest version of Node supported
176+
for (const minVersion of Object.keys(ENGINE_RULES[rule]).sort(semver.compare)) {
177+
if (!semver.intersects(opts.engines.node, `<${minVersion}`)) {
178+
config.rules[rule] = ENGINE_RULES[rule][minVersion];
179+
}
180+
}
181+
}
182+
}
183+
134184
if (opts.space) {
135185
config.rules.indent = ['error', spaces, {SwitchCase: 1}];
136186

main.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const updateNotifier = require('update-notifier');
44
const getStdin = require('get-stdin');
55
const meow = require('meow');
66
const formatterPretty = require('eslint-formatter-pretty');
7+
const semver = require('semver');
78
const openReport = require('./lib/open-report');
89
const xo = require('.');
910

@@ -21,6 +22,7 @@ const cli = meow(`
2122
--space Use space indent instead of tabs [Default: 2]
2223
--no-semicolon Prevent use of semicolons
2324
--prettier Conform to Prettier code style
25+
--node-version Range of Node version to support
2426
--plugin Include third-party plugins [Can be set multiple times]
2527
--extend Extend defaults with a custom config [Can be set multiple times]
2628
--open Open files with issues in your editor
@@ -76,6 +78,9 @@ const cli = meow(`
7678
prettier: {
7779
type: 'boolean'
7880
},
81+
nodeVersion: {
82+
type: 'string'
83+
},
7984
plugin: {
8085
type: 'string'
8186
},
@@ -136,6 +141,17 @@ if (input[0] === '-') {
136141
input.shift();
137142
}
138143

144+
if (opts.nodeVersion) {
145+
if (opts.nodeVersion === 'false') {
146+
opts.engines = false;
147+
} else if (semver.validRange(opts.nodeVersion)) {
148+
opts.engines = {node: opts.nodeVersion};
149+
} else {
150+
console.error('The `node-engine` option must be a valid semver range (for example `>=4`)');
151+
process.exit(1);
152+
}
153+
}
154+
139155
if (opts.init) {
140156
require('xo-init')();
141157
} else if (opts.stdin) {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"prettier": "~1.9.2",
9898
"resolve-cwd": "^2.0.0",
9999
"resolve-from": "^4.0.0",
100+
"semver": "^5.4.1",
100101
"slash": "^1.0.0",
101102
"update-notifier": "^2.1.0",
102103
"xo-init": "^0.6.0"

readme.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Uses [ESLint](http://eslint.org) underneath, so issues regarding rules should be
2727
- No need to specify file paths to lint as it lints all JS files except for [commonly ignored paths](#ignores).
2828
- [Config overrides per files/globs.](#config-overrides)
2929
- Includes many useful ESLint plugins, like [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn), [`import`](https://github.com/benmosher/eslint-plugin-import), [`ava`](https://github.com/avajs/eslint-plugin-ava), [`node`](https://github.com/mysticatea/eslint-plugin-node) and more.
30+
- Automatically enable or disable rules based on the [engines](https://docs.npmjs.com/files/package.json#engines) of your `package.json`.
3031
- Caches results between runs for much better performance.
3132
- Super simple to add XO to a project with `$ xo --init`.
3233
- Fix many issues automagically with `$ xo --fix`.
@@ -61,6 +62,7 @@ $ xo --help
6162
--space Use space indent instead of tabs [Default: 2]
6263
--no-semicolon Prevent use of semicolons
6364
--prettier Conform to Prettier code style
65+
--node-version Range of Node version to support
6466
--plugin Include third-party plugins [Can be set multiple times]
6567
--extend Extend defaults with a custom config [Can be set multiple times]
6668
--open Open files with issues in your editor
@@ -206,6 +208,14 @@ Default: `false`
206208

207209
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)
208210

211+
### nodeVersion
212+
213+
Type: `string`, `boolean`<br>
214+
Default: value of `engines.node` key in the project `package.json`
215+
216+
Enable rules specific to the Node versions within the range configured.
217+
If set to `false` no rules specific to a Node version will be enable.
218+
209219
### plugins
210220

211221
Type: `Array`
@@ -303,6 +313,25 @@ If you have a directory structure with nested `package.json` files and you want
303313

304314
Put a `package.json` with your config at the root and add `"xo": false` to the `package.json` in your bundled packages.
305315

316+
### Transpilation
317+
318+
If some files in your project are transpiled in order to support an older Node version you can use the [Config Overrides](#config-overrides) option to set a specific [nodeVersion](#nodeversion) target to these files.
319+
320+
For example, if your project targets Node 4 (your `package.json` is configured with `engines.node` to `>=4`) and you are using [AVA](https://github.com/avajs/ava), then your test files are automatically transpiled. You can override `nodeVersion` for the tests files:
321+
322+
```json
323+
{
324+
"xo": {
325+
"overrides": [
326+
{
327+
"files": "{test,tests,spec,__tests__}/**/*.js",
328+
"nodeVersion": ">=9"
329+
}
330+
]
331+
}
332+
}
333+
```
334+
306335

307336
## FAQ
308337

test/fixtures/engines/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "application-name",
3+
"version": "0.0.1",
4+
"engines": {
5+
"node": ">=6"
6+
}
7+
}

test/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,9 @@ test('init option', async t => {
9494
const packageJson = fs.readFileSync(filepath, 'utf8');
9595
t.deepEqual(JSON.parse(packageJson).scripts, {test: 'xo'});
9696
});
97+
98+
test('invalid node-engine option', async t => {
99+
const filepath = await tempWrite('console.log()\n', 'x.js');
100+
const err = await t.throws(main(['--node-version', 'v', filepath]));
101+
t.is(err.code, 1);
102+
});

test/options-manager.js

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import proxyquire from 'proxyquire';
44
import parentConfig from './fixtures/nested/package';
55
import childConfig from './fixtures/nested/child/package';
66
import prettierConfig from './fixtures/prettier/package';
7+
import enginesConfig from './fixtures/engines/package';
78

89
process.chdir(__dirname);
910

@@ -138,6 +139,88 @@ test('buildConfig: prettier: true, esnext: false', t => {
138139
}]);
139140
});
140141

142+
test('buildConfig: engines: undefined', t => {
143+
const config = manager.buildConfig({});
144+
145+
// Do not include any node version specific rules
146+
t.is(config.rules['prefer-spread'], undefined);
147+
t.is(config.rules['prefer-rest-params'], undefined);
148+
t.is(config.rules['prefer-destructuring'], undefined);
149+
t.is(config.rules['promise/prefer-await-to-then'], undefined);
150+
});
151+
152+
test('buildConfig: engines: false', t => {
153+
const config = manager.buildConfig({engines: false});
154+
155+
// Do not include any node version specific rules
156+
t.is(config.rules['prefer-spread'], undefined);
157+
t.is(config.rules['prefer-rest-params'], undefined);
158+
t.is(config.rules['prefer-destructuring'], undefined);
159+
t.is(config.rules['promise/prefer-await-to-then'], undefined);
160+
});
161+
162+
test('buildConfig: engines: invalid range', t => {
163+
const config = manager.buildConfig({engines: {node: '4'}});
164+
165+
// Do not include any node version specific rules
166+
t.is(config.rules['prefer-spread'], undefined);
167+
t.is(config.rules['prefer-rest-params'], undefined);
168+
t.is(config.rules['prefer-destructuring'], undefined);
169+
t.is(config.rules['promise/prefer-await-to-then'], undefined);
170+
});
171+
172+
test('buildConfig: engines: >=4', t => {
173+
const config = manager.buildConfig({engines: {node: '>=4'}});
174+
175+
// Do not include rules for Node 5 and above
176+
t.is(config.rules['prefer-spread'], undefined);
177+
// Do not include rules for Node 6 and above
178+
t.is(config.rules['prefer-rest-params'], undefined);
179+
t.is(config.rules['prefer-destructuring'], undefined);
180+
// Do not include rules for Node 8 and above
181+
t.is(config.rules['promise/prefer-await-to-then'], undefined);
182+
});
183+
184+
test('buildConfig: engines: >=4.1', t => {
185+
const config = manager.buildConfig({engines: {node: '>=5.1'}});
186+
187+
// Do not include rules for Node 5 and above
188+
t.is(config.rules['prefer-spread'], 'error');
189+
// Do not include rules for Node 6 and above
190+
t.is(config.rules['prefer-rest-params'], undefined);
191+
t.is(config.rules['prefer-destructuring'], undefined);
192+
// Do not include rules for Node 8 and above
193+
t.is(config.rules['promise/prefer-await-to-then'], undefined);
194+
});
195+
196+
test('buildConfig: engines: >=6', t => {
197+
const config = manager.buildConfig({engines: {node: '>=6'}});
198+
199+
// Include rules for Node 5 and above
200+
t.is(config.rules['prefer-spread'], 'error');
201+
// Include rules for Node 6 and above
202+
t.is(config.rules['prefer-rest-params'], 'error');
203+
t.deepEqual(config.rules['prefer-destructuring'], [
204+
'error', {array: true, object: true}, {enforceForRenamedProperties: true}
205+
]);
206+
// Do not include rules for Node 8 and above
207+
t.is(config.rules['promise/prefer-await-to-then'], undefined);
208+
});
209+
210+
test('buildConfig: engines: >=8', t => {
211+
const config = manager.buildConfig({engines: {node: '>=8'}});
212+
213+
// Include rules for Node 5 and above
214+
t.is(config.rules['prefer-spread'], 'error');
215+
// Include rules for Node 6 and above
216+
t.is(config.rules['prefer-rest-params'], 'error');
217+
t.deepEqual(config.rules['prefer-destructuring'], [
218+
'error', {array: true, object: true}, {enforceForRenamedProperties: true}
219+
]);
220+
// Include rules for Node 8 and above
221+
t.is(config.rules['promise/prefer-await-to-then'], 'error');
222+
});
223+
141224
test('mergeWithPrettierConf: use `singleQuote`, `trailingComma`, `bracketSpacing` and `jsxBracketSameLine` from `prettier` config if defined', t => {
142225
const cwd = path.resolve('fixtures', 'prettier');
143226
const result = manager.mergeWithPrettierConf({cwd});
@@ -255,26 +338,47 @@ test('groupConfigs', t => {
255338
test('mergeWithPkgConf: use child if closest', t => {
256339
const cwd = path.resolve('fixtures', 'nested', 'child');
257340
const result = manager.mergeWithPkgConf({cwd});
258-
const expected = Object.assign({}, childConfig.xo, {cwd});
341+
const expected = Object.assign({}, childConfig.xo, {cwd}, {engines: {}});
259342
t.deepEqual(result, expected);
260343
});
261344

262345
test('mergeWithPkgConf: use parent if closest', t => {
263346
const cwd = path.resolve('fixtures', 'nested');
264347
const result = manager.mergeWithPkgConf({cwd});
265-
const expected = Object.assign({}, parentConfig.xo, {cwd});
348+
const expected = Object.assign({}, parentConfig.xo, {cwd}, {engines: {}});
266349
t.deepEqual(result, expected);
267350
});
268351

269352
test('mergeWithPkgConf: use parent if child is ignored', t => {
270353
const cwd = path.resolve('fixtures', 'nested', 'child-ignore');
271354
const result = manager.mergeWithPkgConf({cwd});
272-
const expected = Object.assign({}, parentConfig.xo, {cwd});
355+
const expected = Object.assign({}, parentConfig.xo, {cwd}, {engines: {}});
273356
t.deepEqual(result, expected);
274357
});
275358

276359
test('mergeWithPkgConf: use child if child is empty', t => {
277360
const cwd = path.resolve('fixtures', 'nested', 'child-empty');
278361
const result = manager.mergeWithPkgConf({cwd});
279-
t.deepEqual(result, {cwd});
362+
t.deepEqual(result, {cwd, engines: {}});
363+
});
364+
365+
test('mergeWithPkgConf: read engines from package.json', t => {
366+
const cwd = path.resolve('fixtures', 'engines');
367+
const result = manager.mergeWithPkgConf({cwd});
368+
const expected = Object.assign({}, {engines: enginesConfig.engines}, {cwd});
369+
t.deepEqual(result, expected);
370+
});
371+
372+
test('mergeWithPkgConf: XO engine options supersede package.json\'s', t => {
373+
const cwd = path.resolve('fixtures', 'engines');
374+
const result = manager.mergeWithPkgConf({cwd, engines: {node: '>=8'}});
375+
const expected = Object.assign({}, {engines: {node: '>=8'}}, {cwd});
376+
t.deepEqual(result, expected);
377+
});
378+
379+
test('mergeWithPkgConf: XO engine options false supersede package.json\'s', t => {
380+
const cwd = path.resolve('fixtures', 'engines');
381+
const result = manager.mergeWithPkgConf({cwd, engines: false});
382+
const expected = Object.assign({}, {engines: false}, {cwd});
383+
t.deepEqual(result, expected);
280384
});

0 commit comments

Comments
 (0)