Skip to content

Commit

Permalink
Merge a1041c1 into 6d25237
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg committed Feb 28, 2020
2 parents 6d25237 + a1041c1 commit 81d007e
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 83 deletions.
50 changes: 32 additions & 18 deletions index.js
Expand Up @@ -3,15 +3,19 @@ const path = require('path');
const eslint = require('eslint');
const globby = require('globby');
const isEqual = require('lodash/isEqual');
const uniq = require('lodash/uniq');
const micromatch = require('micromatch');
const arrify = require('arrify');
const {DEFAULT_EXTENSION} = require('./lib/constants');
const pReduce = require('p-reduce');
const {cosmiconfig, defaultLoaders} = require('cosmiconfig');
const {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} = require('./lib/constants');
const {
normalizeOptions,
getIgnores,
mergeWithFileConfig,
mergeWithFileConfigs,
buildConfig
buildConfig,
mergeOptions
} = require('./lib/options-manager');

const mergeReports = reports => {
Expand Down Expand Up @@ -47,6 +51,12 @@ const runEslint = (paths, options) => {
return processReport(report, options);
};

const globFiles = async (patterns, {ignores, extensions, cwd}) => (
await globby(
patterns.length === 0 ? [`**/*.{${extensions.join(',')}}`] : arrify(patterns),
{ignore: ignores, gitignore: true, cwd}
)).filter(file => extensions.includes(path.extname(file).slice(1))).map(file => path.resolve(cwd, file));

const lintText = (string, options) => {
const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(options));
options = buildConfig(foundOptions, prettierOptions);
Expand Down Expand Up @@ -83,22 +93,26 @@ const lintText = (string, options) => {
return processReport(report, options);
};

const lintFiles = async (patterns, options) => {
options = normalizeOptions(options);

const isEmptyPatterns = patterns.length === 0;
const defaultPattern = `**/*.{${DEFAULT_EXTENSION.concat(options.extensions || []).join(',')}}`;

const paths = await globby(
isEmptyPatterns ? [defaultPattern] : arrify(patterns),
{
ignore: getIgnores(options),
gitignore: true,
cwd: options.cwd || process.cwd()
}
);

return mergeReports((await mergeWithFileConfigs(paths, options)).map(
const lintFiles = async (patterns, options = {}) => {
options.cwd = path.resolve(options.cwd || process.cwd());
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});

const configFiles = (await Promise.all(
(await globby(
CONFIG_FILES.map(configFile => `**/${configFile}`),
{ignore: DEFAULT_IGNORES, gitignore: true, cwd: options.cwd}
)).map(async configFile => configExplorer.load(path.resolve(options.cwd, configFile)))
)).filter(Boolean);

const paths = configFiles.length > 0 ?
await pReduce(
configFiles,
async (paths, {filepath, config}) =>
[...paths, ...(await globFiles(patterns, {...mergeOptions(options, config), cwd: path.dirname(filepath)}))],
[]) :
await globFiles(patterns, mergeOptions(options));

return mergeReports((await mergeWithFileConfigs(uniq(paths), options, configFiles)).map(
({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions)))
);
};
Expand Down
30 changes: 12 additions & 18 deletions lib/options-manager.js
Expand Up @@ -19,6 +19,7 @@ const JSON5 = require('json5');
const toAbsoluteGlob = require('to-absolute-glob');
const stringify = require('json-stable-stringify-without-jsonify');
const murmur = require('imurmurhash');
const isPathInside = require('is-path-inside');
const {
DEFAULT_IGNORES,
DEFAULT_EXTENSION,
Expand Down Expand Up @@ -89,7 +90,7 @@ const mergeWithFileConfig = options => {
const {config: xoOptions, filepath: xoConfigPath} = configExplorer.search(searchPath) || {};
const {config: enginesOptions} = pkgConfigExplorer.search(searchPath) || {};

options = mergeOptions(xoOptions, enginesOptions, options);
options = mergeOptions(options, xoOptions, enginesOptions);
options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd;

if (options.filename) {
Expand All @@ -114,26 +115,19 @@ const mergeWithFileConfig = options => {
Find config for each files found by `lintFiles`.
The config files are searched starting from each files.
*/
const mergeWithFileConfigs = async (files, options) => {
options.cwd = path.resolve(options.cwd || process.cwd());

const mergeWithFileConfigs = async (files, options, configFiles) => {
configFiles = configFiles.sort((a, b) => b.filepath.split(path.sep).length - a.filepath.split(path.sep).length);
const tsConfigs = {};

const groups = [...(await pReduce(files.map(file => path.resolve(options.cwd, file)), async (configs, file) => {
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});
const groups = [...(await pReduce(files, async (configs, file) => {
const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});

const {config: xoOptions, filepath: xoConfigPath} = await configExplorer.search(file) || {};
const {config: xoOptions, filepath: xoConfigPath} = findApplicableConfig(file, configFiles) || {};
const {config: enginesOptions, filepath: enginesConfigPath} = await pkgConfigExplorer.search(file) || {};

let fileOptions = mergeOptions(xoOptions, enginesOptions, options);
let fileOptions = mergeOptions(options, xoOptions, enginesOptions);
fileOptions.cwd = xoConfigPath && path.dirname(xoConfigPath) !== fileOptions.cwd ? path.resolve(fileOptions.cwd, path.dirname(xoConfigPath)) : fileOptions.cwd;

if (!fileOptions.extensions.includes(path.extname(file).replace('.', '')) || isFileIgnored(file, fileOptions)) {
// File extension/path is ignored, skip it
return configs;
}

const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions);
fileOptions = optionsWithOverrides;

Expand All @@ -143,7 +137,6 @@ const mergeWithFileConfigs = async (files, options) => {
let tsConfigPath;
if (isTypescript(file)) {
let tsConfig;
// Override cosmiconfig `loaders` as we look only for the path of tsconfig.json, but not its content
const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {});

Expand Down Expand Up @@ -178,6 +171,8 @@ const mergeWithFileConfigs = async (files, options) => {
return groups;
};

const findApplicableConfig = (file, configFiles) => configFiles.find(({filepath}) => isPathInside(file, path.dirname(filepath)));

/**
Generate a unique and consistent path for the temporary `tsconfig.json`.
Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0143fd896fccd771/lib/cli-engine/lint-result-cache.js#L30
Expand Down Expand Up @@ -237,12 +232,10 @@ const normalizeOptions = options => {

const normalizeSpaces = options => typeof options.space === 'number' ? options.space : 2;

const isFileIgnored = (file, options) => micromatch.isMatch(path.relative(options.cwd, file), options.ignores);

/**
Merge option passed via CLI/API via options founf in config files.
*/
const mergeOptions = (xoOptions, enginesOptions, options) => {
const mergeOptions = (options, xoOptions = {}, enginesOptions = {}) => {
const mergedOptions = normalizeOptions({
...xoOptions,
nodeVersion: enginesOptions && enginesOptions.node && semver.validRange(enginesOptions.node),
Expand Down Expand Up @@ -491,5 +484,6 @@ module.exports = {
mergeWithFileConfigs,
mergeWithFileConfig,
buildConfig,
applyOverrides
applyOverrides,
mergeOptions
};
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -75,6 +75,7 @@
"globby": "^9.0.0",
"has-flag": "^4.0.0",
"imurmurhash": "^0.1.4",
"is-path-inside": "^3.0.2",
"json-stable-stringify-without-jsonify": "^1.0.1",
"json5": "^2.1.1",
"lodash": "^4.17.15",
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/config-files/xo-config/file.js
@@ -0,0 +1,2 @@
var obj = { a: 1 };
console.log(obj.a);
2 changes: 2 additions & 0 deletions test/fixtures/config-files/xo-config_js/file.js
@@ -0,0 +1,2 @@
var obj = { a: 1 };
console.log(obj.a);
2 changes: 2 additions & 0 deletions test/fixtures/config-files/xo-config_json/file.js
@@ -0,0 +1,2 @@
var obj = { a: 1 };
console.log(obj.a);
2 changes: 2 additions & 0 deletions test/fixtures/config-files/xo_config_js/file.js
@@ -0,0 +1,2 @@
var obj = { a: 1 };
console.log(obj.a);
19 changes: 19 additions & 0 deletions test/lint-files.js
Expand Up @@ -195,3 +195,22 @@ test('typescript files', async t => {
)
);
});

async function configType(t, {dir}) {
const {results} = await fn.lintFiles('**/*', {cwd: path.resolve('fixtures', 'config-files', dir)});

t.true(
hasRule(
results,
path.resolve('fixtures', 'config-files', dir, 'file.js'),
'no-var'
)
);
}

configType.title = (_, {type}) => `load config from ${type}`.trim();

test(configType, {type: 'xo.config.js', dir: 'xo-config_js'});
test(configType, {type: '.xo-config.js', dir: 'xo-config_js'});
test(configType, {type: '.xo-config.json', dir: 'xo-config_json'});
test(configType, {type: '.xo-config', dir: 'xo-config'});
12 changes: 12 additions & 0 deletions test/lint-text.js
Expand Up @@ -282,3 +282,15 @@ test('typescript files', t => {
]);`, {filename: 'fixtures/typescript/child/sub-child/four-spaces.ts'}));
t.true(hasRule(results, '@typescript-eslint/indent'));
});

function configType(t, {dir}) {
const {results} = fn.lintText('var obj = { a: 1 };\n', {cwd: path.resolve('fixtures', 'config-files', dir), filename: 'file.js'});
t.true(hasRule(results, 'no-var'));
}

configType.title = (_, {type}) => `load config from ${type}`.trim();

test(configType, {type: 'xo.config.js', dir: 'xo-config_js'});
test(configType, {type: '.xo-config.js', dir: 'xo-config_js'});
test(configType, {type: '.xo-config.json', dir: 'xo-config_json'});
test(configType, {type: '.xo-config', dir: 'xo-config'});
68 changes: 21 additions & 47 deletions test/options-manager.js
Expand Up @@ -521,29 +521,26 @@ test('mergeWithFileConfig: tsx files', async t => {
});
});

function mergeWithFileConfigFileType(t, {dir}) {
const cwd = path.resolve('fixtures', 'config-files', dir);
const {options} = manager.mergeWithFileConfig({cwd});
const expected = {esnext: true, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, cwd, nodeVersion: undefined};
t.deepEqual(options, expected);
}

mergeWithFileConfigFileType.title = (_, {type}) => `mergeWithFileConfig: load from ${type}`.trim();

test(mergeWithFileConfigFileType, {type: 'xo.config.js', dir: 'xo-config_js'});
test(mergeWithFileConfigFileType, {type: '.xo-config.js', dir: 'xo-config_js'});
test(mergeWithFileConfigFileType, {type: '.xo-config.json', dir: 'xo-config_json'});
test(mergeWithFileConfigFileType, {type: '.xo-config', dir: 'xo-config'});

test('mergeWithFileConfigs: nested configs with prettier', async t => {
const cwd = path.resolve('fixtures', 'nested-configs');
const paths = [
'no-semicolon.js',
'child/semicolon.js',
'child-override/two-spaces.js',
'child-override/child-prettier-override/semicolon.js'
];
const result = await manager.mergeWithFileConfigs(paths, {cwd});
].map(file => path.resolve(cwd, file));
const result = await manager.mergeWithFileConfigs(paths, {cwd}, [
{
filepath: path.resolve(cwd, 'child-override', 'child-prettier-override', 'package.json'),
config: {overrides: [{files: 'semicolon.js', prettier: true}]}
},
{filepath: path.resolve(cwd, 'package.json'), config: {semicolon: true}},
{
filepath: path.resolve(cwd, 'child-override', 'package.json'),
config: {overrides: [{files: 'two-spaces.js', space: 4}]}
},
{filepath: path.resolve(cwd, 'child', 'package.json'), config: {semicolon: false}}
]);

t.deepEqual(result, [
{
Expand Down Expand Up @@ -607,8 +604,13 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => {

test('mergeWithFileConfigs: typescript files', async t => {
const cwd = path.resolve('fixtures', 'typescript');
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts', 'child/sub-child/four-spaces.ts'];
const result = await manager.mergeWithFileConfigs(paths, {cwd});
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts', 'child/sub-child/four-spaces.ts'].map(file => path.resolve(cwd, file));
const configFiles = [
{filepath: path.resolve(cwd, 'child/sub-child/package.json'), config: {space: 2}},
{filepath: path.resolve(cwd, 'package.json'), config: {space: 4}},
{filepath: path.resolve(cwd, 'child/package.json'), config: {semicolon: false}}
];
const result = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);

t.deepEqual(omit(result[0], 'options.tsConfigPath'), {
files: [path.resolve(cwd, 'two-spaces.tsx')],
Expand Down Expand Up @@ -672,41 +674,13 @@ test('mergeWithFileConfigs: typescript files', async t => {
]
});

const secondResult = await manager.mergeWithFileConfigs(paths, {cwd});
const secondResult = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles);

// Verify that on each run the options.tsConfigPath is consistent to preserve ESLint cache
t.is(result[0].options.tsConfigPath, secondResult[0].options.tsConfigPath);
t.is(result[1].options.tsConfigPath, secondResult[1].options.tsConfigPath);
});

async function mergeWithFileConfigsFileType(t, {dir}) {
const cwd = path.resolve('fixtures', 'config-files', dir);
const paths = ['a.js', 'b.js'];

const result = await manager.mergeWithFileConfigs(paths, {cwd});

t.deepEqual(result, [
{
files: paths.reverse().map(p => path.resolve(cwd, p)),
options: {
esnext: true,
nodeVersion: undefined,
cwd,
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES
},
prettierOptions: {}
}
]);
}

mergeWithFileConfigsFileType.title = (_, {type}) => `mergeWithFileConfigs: load from ${type}`.trim();

test(mergeWithFileConfigsFileType, {type: 'xo.config.js', dir: 'xo-config_js'});
test(mergeWithFileConfigsFileType, {type: '.xo-config.js', dir: 'xo-config_js'});
test(mergeWithFileConfigsFileType, {type: '.xo-config.json', dir: 'xo-config_json'});
test(mergeWithFileConfigsFileType, {type: '.xo-config', dir: 'xo-config'});

test('applyOverrides', t => {
t.deepEqual(
manager.applyOverrides(
Expand Down

0 comments on commit 81d007e

Please sign in to comment.