Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various fixes to TypeScript integration #431

Merged
merged 4 commits into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 0 additions & 5 deletions config/default.tsconfig.json

This file was deleted.

61 changes: 41 additions & 20 deletions lib/options-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
const os = require('os');
const path = require('path');
const {outputJson, outputJsonSync} = require('fs-extra');
const pkg = require('../package.json');
const arrify = require('arrify');
const mergeWith = require('lodash/mergeWith');
const groupBy = require('lodash/groupBy');
const flow = require('lodash/flow');
const pathExists = require('path-exists');
const findCacheDir = require('find-cache-dir');
Expand All @@ -15,7 +17,8 @@ const pReduce = require('p-reduce');
const micromatch = require('micromatch');
const JSON5 = require('json5');
const toAbsoluteGlob = require('to-absolute-glob');
const tempy = require('tempy');
const stringify = require('json-stable-stringify-without-jsonify');
const murmur = require('imurmurhash');
const {
DEFAULT_IGNORES,
DEFAULT_EXTENSION,
Expand All @@ -28,10 +31,13 @@ const {
TSCONFIG_DEFFAULTS
} = require('./constants');

const nodeVersion = process && process.version;
const cacheLocation = findCacheDir({name: 'xo'}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/');

const DEFAULT_CONFIG = {
useEslintrc: false,
cache: true,
cacheLocation: findCacheDir({name: 'xo'}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/'),
cacheLocation: path.join(cacheLocation, 'xo-cache.json'),
globInputPaths: false,
baseConfig: {
extends: [
Expand Down Expand Up @@ -96,7 +102,7 @@ const mergeWithFileConfig = options => {
const tsConfigExplorer = cosmiconfigSync([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
const {config: tsConfig, filepath: tsConfigPath} = tsConfigExplorer.search(options.filename) || {};

options.tsConfigPath = tempy.file({name: 'tsconfig.json'});
options.tsConfigPath = getTsConfigCachePath([options.filename], options.tsConfigPath);
options.ts = true;
outputJsonSync(options.tsConfigPath, makeTSConfig(tsConfig, tsConfigPath, [options.filename]));
}
Expand All @@ -111,7 +117,9 @@ The config files are searched starting from each files.
const mergeWithFileConfigs = async (files, options) => {
options.cwd = path.resolve(options.cwd || process.cwd());

return Promise.all([...(await pReduce(files.map(file => path.resolve(options.cwd, file)), async (configs, file) => {
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 pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});

Expand All @@ -127,8 +135,9 @@ const mergeWithFileConfigs = async (files, options) => {
}

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

const prettierConfigPath = optionsWithOverrides.prettier ? await prettier.resolveConfigFile(file) : undefined;
const prettierConfigPath = fileOptions.prettier ? await prettier.resolveConfigFile(file) : undefined;
const prettierOptions = prettierConfigPath ? await prettier.resolveConfig(file, {config: prettierConfigPath}) : {};

let tsConfigPath;
Expand All @@ -138,38 +147,50 @@ const mergeWithFileConfigs = async (files, options) => {
const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {});

optionsWithOverrides.tsConfigPath = tsConfigPath;
optionsWithOverrides.tsConfig = tsConfig;
optionsWithOverrides.ts = true;
fileOptions.tsConfigPath = tsConfigPath;
tsConfigs[tsConfigPath || ''] = tsConfig;
fileOptions.ts = true;
}

const cacheKey = JSON.stringify({xoConfigPath, enginesConfigPath, prettierConfigPath, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
const cacheKey = stringify({xoConfigPath, enginesConfigPath, prettierConfigPath, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
const cachedGroup = configs.get(cacheKey);

configs.set(cacheKey, {
files: [file, ...(cachedGroup ? cachedGroup.files : [])],
options: cachedGroup ? cachedGroup.options : optionsWithOverrides,
options: cachedGroup ? cachedGroup.options : fileOptions,
prettierOptions
});

return configs;
}, new Map())).values()].map(async group => {
const {files, options} = group;
if (options.ts) {
const tsConfigPath = tempy.file({name: 'tsconfig.json'});
await outputJson(tsConfigPath, makeTSConfig(options.tsConfig, options.tsConfigPath, files));
group.options.tsConfigPath = tsConfigPath;
delete group.options.tsConfig;
}, new Map())).values()];

await Promise.all(Object.entries(groupBy(groups.filter(({options}) => Boolean(options.ts)), group => group.options.tsConfigPath || '')).map(
([tsConfigPath, groups]) => {
const files = [].concat(...groups.map(group => group.files));
const cachePath = getTsConfigCachePath(files, tsConfigPath);
groups.forEach(group => {
group.options.tsConfigPath = cachePath;
});
return outputJson(cachePath, makeTSConfig(tsConfigs[tsConfigPath], tsConfigPath, files));
}
));

return group;
}));
return groups;
};

/**
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
*/
const getTsConfigCachePath = (files, tsConfigPath) => path.join(
cacheLocation,
`tsconfig.${murmur(`${pkg.version}_${nodeVersion}_${stringify({files, tsConfigPath: tsConfigPath})}`).result().toString(36)}.json`
);

const makeTSConfig = (tsConfig, tsConfigPath, files) => {
const config = {files: files.filter(isTypescript)};

if (tsConfigPath) {
if (tsConfig) {
config.extends = tsConfigPath;
config.include = arrify(tsConfig.include).map(pattern => toAbsoluteGlob(pattern, {cwd: path.dirname(tsConfigPath)}));
} else {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"get-stdin": "^7.0.0",
"globby": "^9.0.0",
"has-flag": "^4.0.0",
"imurmurhash": "^0.1.4",
"json-stable-stringify-without-jsonify": "^1.0.1",
"json5": "^2.1.1",
"lodash": "^4.17.15",
"meow": "^5.0.0",
Expand All @@ -86,7 +88,6 @@
"resolve-from": "^5.0.0",
"semver": "^7.1.3",
"slash": "^3.0.0",
"tempy": "^0.4.0",
"to-absolute-glob": "^2.0.2",
"update-notifier": "^4.0.0"
},
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/typescript/child/sub-child/four-spaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
console.log([
4
]);
5 changes: 5 additions & 0 deletions test/fixtures/typescript/child/sub-child/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"xo": {
"space": 2
}
}
8 changes: 8 additions & 0 deletions test/lint-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,12 @@ test('typescript files', async t => {
'@typescript-eslint/no-extra-semi'
)
);

t.true(
hasRule(
results,
path.resolve('fixtures/typescript/child/sub-child/four-spaces.ts'),
'@typescript-eslint/indent'
)
);
});
11 changes: 8 additions & 3 deletions test/lint-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,16 @@ test('find configurations close to linted file', t => {
});

test('typescript files', t => {
let {results} = fn.lintText('console.log(\'extra-semicolon\');;\n', {filename: 'fixtures/typescript/child/extra-semicolon.ts'});
let {results} = fn.lintText(`console.log([
2
]);`, {filename: 'fixtures/typescript/two-spaces.tsx'});
t.true(hasRule(results, '@typescript-eslint/indent'));

({results} = fn.lintText('console.log(\'extra-semicolon\');;\n', {filename: 'fixtures/typescript/child/extra-semicolon.ts'}));
t.true(hasRule(results, '@typescript-eslint/no-extra-semi'));

({results} = fn.lintText(`console.log([
2
]);`, {filename: 'fixtures/typescript/two-spaces.tsx'}));
4
]);`, {filename: 'fixtures/typescript/child/sub-child/four-spaces.ts'}));
t.true(hasRule(results, '@typescript-eslint/indent'));
});
28 changes: 25 additions & 3 deletions test/options-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test('normalizeOptions: falsie values stay falsie', t => {

test('buildConfig: defaults', t => {
const config = manager.buildConfig({});
t.regex(slash(config.cacheLocation), /[\\/]\.cache\/xo[\\/]?$/u);
t.regex(slash(config.cacheLocation), /[\\/]\.cache\/xo\/xo-cache.json[\\/]?$/u);
t.is(config.useEslintrc, false);
t.is(config.cache, true);
t.is(config.baseConfig.extends[0], 'xo/esnext');
Expand Down Expand Up @@ -607,7 +607,7 @@ 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'];
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts', 'child/sub-child/four-spaces.ts'];
const result = await manager.mergeWithFileConfigs(paths, {cwd});

t.deepEqual(omit(result[0], 'options.tsConfigPath'), {
Expand Down Expand Up @@ -647,14 +647,36 @@ test('mergeWithFileConfigs: typescript files', async t => {
},
prettierOptions: {}
});

t.deepEqual(omit(result[2], 'options.tsConfigPath'), {
files: [path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
options: {
space: 2,
nodeVersion: undefined,
cwd: path.resolve(cwd, 'child/sub-child'),
extensions: DEFAULT_EXTENSION,
ignores: DEFAULT_IGNORES,
ts: true
},
prettierOptions: {}
});

// Verify that we use the same temporary tsconfig.json for both files group sharing the same original tsconfig.json even if they have different xo config
t.is(result[1].options.tsConfigPath, result[2].options.tsConfigPath);
t.deepEqual(await readJson(result[1].options.tsConfigPath), {
extends: path.resolve(cwd, 'child/tsconfig.json'),
files: [path.resolve(cwd, 'child/extra-semicolon.ts')],
files: [path.resolve(cwd, 'child/extra-semicolon.ts'), path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
include: [
slash(path.resolve(cwd, 'child/**/*.ts')),
slash(path.resolve(cwd, 'child/**/*.tsx'))
]
});

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

// 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}) {
Expand Down