Skip to content

Commit 0ea5dbb

Browse files
authoredFeb 25, 2020
Various fixes to TypeScript integration (#431)
1 parent ee7b08e commit 0ea5dbb

File tree

8 files changed

+92
-32
lines changed

8 files changed

+92
-32
lines changed
 

Diff for: ‎config/default.tsconfig.json

-5
This file was deleted.

Diff for: ‎lib/options-manager.js

+41-20
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
const os = require('os');
33
const path = require('path');
44
const {outputJson, outputJsonSync} = require('fs-extra');
5+
const pkg = require('../package.json');
56
const arrify = require('arrify');
67
const mergeWith = require('lodash/mergeWith');
8+
const groupBy = require('lodash/groupBy');
79
const flow = require('lodash/flow');
810
const pathExists = require('path-exists');
911
const findCacheDir = require('find-cache-dir');
@@ -15,7 +17,8 @@ const pReduce = require('p-reduce');
1517
const micromatch = require('micromatch');
1618
const JSON5 = require('json5');
1719
const toAbsoluteGlob = require('to-absolute-glob');
18-
const tempy = require('tempy');
20+
const stringify = require('json-stable-stringify-without-jsonify');
21+
const murmur = require('imurmurhash');
1922
const {
2023
DEFAULT_IGNORES,
2124
DEFAULT_EXTENSION,
@@ -28,10 +31,13 @@ const {
2831
TSCONFIG_DEFFAULTS
2932
} = require('./constants');
3033

34+
const nodeVersion = process && process.version;
35+
const cacheLocation = findCacheDir({name: 'xo'}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/');
36+
3137
const DEFAULT_CONFIG = {
3238
useEslintrc: false,
3339
cache: true,
34-
cacheLocation: findCacheDir({name: 'xo'}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/'),
40+
cacheLocation: path.join(cacheLocation, 'xo-cache.json'),
3541
globInputPaths: false,
3642
baseConfig: {
3743
extends: [
@@ -96,7 +102,7 @@ const mergeWithFileConfig = options => {
96102
const tsConfigExplorer = cosmiconfigSync([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
97103
const {config: tsConfig, filepath: tsConfigPath} = tsConfigExplorer.search(options.filename) || {};
98104

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

114-
return Promise.all([...(await pReduce(files.map(file => path.resolve(options.cwd, file)), async (configs, file) => {
120+
const tsConfigs = {};
121+
122+
const groups = [...(await pReduce(files.map(file => path.resolve(options.cwd, file)), async (configs, file) => {
115123
const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});
116124
const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});
117125

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

129137
const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions);
138+
fileOptions = optionsWithOverrides;
130139

131-
const prettierConfigPath = optionsWithOverrides.prettier ? await prettier.resolveConfigFile(file) : undefined;
140+
const prettierConfigPath = fileOptions.prettier ? await prettier.resolveConfigFile(file) : undefined;
132141
const prettierOptions = prettierConfigPath ? await prettier.resolveConfig(file, {config: prettierConfigPath}) : {};
133142

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

141-
optionsWithOverrides.tsConfigPath = tsConfigPath;
142-
optionsWithOverrides.tsConfig = tsConfig;
143-
optionsWithOverrides.ts = true;
150+
fileOptions.tsConfigPath = tsConfigPath;
151+
tsConfigs[tsConfigPath || ''] = tsConfig;
152+
fileOptions.ts = true;
144153
}
145154

146-
const cacheKey = JSON.stringify({xoConfigPath, enginesConfigPath, prettierConfigPath, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
155+
const cacheKey = stringify({xoConfigPath, enginesConfigPath, prettierConfigPath, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
147156
const cachedGroup = configs.get(cacheKey);
148157

149158
configs.set(cacheKey, {
150159
files: [file, ...(cachedGroup ? cachedGroup.files : [])],
151-
options: cachedGroup ? cachedGroup.options : optionsWithOverrides,
160+
options: cachedGroup ? cachedGroup.options : fileOptions,
152161
prettierOptions
153162
});
154163

155164
return configs;
156-
}, new Map())).values()].map(async group => {
157-
const {files, options} = group;
158-
if (options.ts) {
159-
const tsConfigPath = tempy.file({name: 'tsconfig.json'});
160-
await outputJson(tsConfigPath, makeTSConfig(options.tsConfig, options.tsConfigPath, files));
161-
group.options.tsConfigPath = tsConfigPath;
162-
delete group.options.tsConfig;
165+
}, new Map())).values()];
166+
167+
await Promise.all(Object.entries(groupBy(groups.filter(({options}) => Boolean(options.ts)), group => group.options.tsConfigPath || '')).map(
168+
([tsConfigPath, groups]) => {
169+
const files = [].concat(...groups.map(group => group.files));
170+
const cachePath = getTsConfigCachePath(files, tsConfigPath);
171+
groups.forEach(group => {
172+
group.options.tsConfigPath = cachePath;
173+
});
174+
return outputJson(cachePath, makeTSConfig(tsConfigs[tsConfigPath], tsConfigPath, files));
163175
}
176+
));
164177

165-
return group;
166-
}));
178+
return groups;
167179
};
168180

181+
/**
182+
Generate a unique and consistent path for the temporary `tsconfig.json`.
183+
Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0143fd896fccd771/lib/cli-engine/lint-result-cache.js#L30
184+
*/
185+
const getTsConfigCachePath = (files, tsConfigPath) => path.join(
186+
cacheLocation,
187+
`tsconfig.${murmur(`${pkg.version}_${nodeVersion}_${stringify({files, tsConfigPath: tsConfigPath})}`).result().toString(36)}.json`
188+
);
189+
169190
const makeTSConfig = (tsConfig, tsConfigPath, files) => {
170191
const config = {files: files.filter(isTypescript)};
171192

172-
if (tsConfigPath) {
193+
if (tsConfig) {
173194
config.extends = tsConfigPath;
174195
config.include = arrify(tsConfig.include).map(pattern => toAbsoluteGlob(pattern, {cwd: path.dirname(tsConfigPath)}));
175196
} else {

Diff for: ‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
"get-stdin": "^7.0.0",
7575
"globby": "^9.0.0",
7676
"has-flag": "^4.0.0",
77+
"imurmurhash": "^0.1.4",
78+
"json-stable-stringify-without-jsonify": "^1.0.1",
7779
"json5": "^2.1.1",
7880
"lodash": "^4.17.15",
7981
"meow": "^5.0.0",
@@ -86,7 +88,6 @@
8688
"resolve-from": "^5.0.0",
8789
"semver": "^7.1.3",
8890
"slash": "^3.0.0",
89-
"tempy": "^0.4.0",
9091
"to-absolute-glob": "^2.0.2",
9192
"update-notifier": "^4.0.0"
9293
},
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
console.log([
2+
4
3+
]);
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"xo": {
3+
"space": 2
4+
}
5+
}

Diff for: ‎test/lint-files.js

+8
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,12 @@ test('typescript files', async t => {
186186
'@typescript-eslint/no-extra-semi'
187187
)
188188
);
189+
190+
t.true(
191+
hasRule(
192+
results,
193+
path.resolve('fixtures/typescript/child/sub-child/four-spaces.ts'),
194+
'@typescript-eslint/indent'
195+
)
196+
);
189197
});

Diff for: ‎test/lint-text.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,16 @@ test('find configurations close to linted file', t => {
269269
});
270270

271271
test('typescript files', t => {
272-
let {results} = fn.lintText('console.log(\'extra-semicolon\');;\n', {filename: 'fixtures/typescript/child/extra-semicolon.ts'});
272+
let {results} = fn.lintText(`console.log([
273+
2
274+
]);`, {filename: 'fixtures/typescript/two-spaces.tsx'});
275+
t.true(hasRule(results, '@typescript-eslint/indent'));
276+
277+
({results} = fn.lintText('console.log(\'extra-semicolon\');;\n', {filename: 'fixtures/typescript/child/extra-semicolon.ts'}));
273278
t.true(hasRule(results, '@typescript-eslint/no-extra-semi'));
274279

275280
({results} = fn.lintText(`console.log([
276-
2
277-
]);`, {filename: 'fixtures/typescript/two-spaces.tsx'}));
281+
4
282+
]);`, {filename: 'fixtures/typescript/child/sub-child/four-spaces.ts'}));
278283
t.true(hasRule(results, '@typescript-eslint/indent'));
279284
});

Diff for: ‎test/options-manager.js

+25-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ test('normalizeOptions: falsie values stay falsie', t => {
4646

4747
test('buildConfig: defaults', t => {
4848
const config = manager.buildConfig({});
49-
t.regex(slash(config.cacheLocation), /[\\/]\.cache\/xo[\\/]?$/u);
49+
t.regex(slash(config.cacheLocation), /[\\/]\.cache\/xo\/xo-cache.json[\\/]?$/u);
5050
t.is(config.useEslintrc, false);
5151
t.is(config.cache, true);
5252
t.is(config.baseConfig.extends[0], 'xo/esnext');
@@ -607,7 +607,7 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => {
607607

608608
test('mergeWithFileConfigs: typescript files', async t => {
609609
const cwd = path.resolve('fixtures', 'typescript');
610-
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts'];
610+
const paths = ['two-spaces.tsx', 'child/extra-semicolon.ts', 'child/sub-child/four-spaces.ts'];
611611
const result = await manager.mergeWithFileConfigs(paths, {cwd});
612612

613613
t.deepEqual(omit(result[0], 'options.tsConfigPath'), {
@@ -647,14 +647,36 @@ test('mergeWithFileConfigs: typescript files', async t => {
647647
},
648648
prettierOptions: {}
649649
});
650+
651+
t.deepEqual(omit(result[2], 'options.tsConfigPath'), {
652+
files: [path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
653+
options: {
654+
space: 2,
655+
nodeVersion: undefined,
656+
cwd: path.resolve(cwd, 'child/sub-child'),
657+
extensions: DEFAULT_EXTENSION,
658+
ignores: DEFAULT_IGNORES,
659+
ts: true
660+
},
661+
prettierOptions: {}
662+
});
663+
664+
// 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
665+
t.is(result[1].options.tsConfigPath, result[2].options.tsConfigPath);
650666
t.deepEqual(await readJson(result[1].options.tsConfigPath), {
651667
extends: path.resolve(cwd, 'child/tsconfig.json'),
652-
files: [path.resolve(cwd, 'child/extra-semicolon.ts')],
668+
files: [path.resolve(cwd, 'child/extra-semicolon.ts'), path.resolve(cwd, 'child/sub-child/four-spaces.ts')],
653669
include: [
654670
slash(path.resolve(cwd, 'child/**/*.ts')),
655671
slash(path.resolve(cwd, 'child/**/*.tsx'))
656672
]
657673
});
674+
675+
const secondResult = await manager.mergeWithFileConfigs(paths, {cwd});
676+
677+
// Verify that on each run the options.tsConfigPath is consistent to preserve ESLint cache
678+
t.is(result[0].options.tsConfigPath, secondResult[0].options.tsConfigPath);
679+
t.is(result[1].options.tsConfigPath, secondResult[1].options.tsConfigPath);
658680
});
659681

660682
async function mergeWithFileConfigsFileType(t, {dir}) {

0 commit comments

Comments
 (0)
Please sign in to comment.