Skip to content

Commit d0d430d

Browse files
jsamar17kjin
authored andcommitted
fix: allow 'gts clean' to follow tsconfig.json dependency chain (#185)
1 parent f7c75ee commit d0d430d

File tree

2 files changed

+183
-9
lines changed

2 files changed

+183
-9
lines changed

src/util.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,77 @@ export function nop() {
3838
/**
3939
* Find the tsconfig.json, read it, and return parsed contents.
4040
* @param rootDir Directory where the tsconfig.json should be found.
41+
* If the tsconfig.json file has an "extends" field hop down the dependency tree
42+
* until it ends or a circular reference is found in which case an error will be
43+
* thrown
4144
*/
4245
export async function getTSConfig(
43-
rootDir: string, customReadFilep?: ReadFileP): Promise<{}> {
44-
const tsconfigPath = path.join(rootDir, 'tsconfig.json');
46+
rootDir: string, customReadFilep?: ReadFileP): Promise<ConfigFile> {
4547
customReadFilep = customReadFilep || readFilep;
46-
const json = await customReadFilep(tsconfigPath, 'utf8');
47-
const contents = JSON.parse(json);
48-
return contents;
48+
const readArr = new Set();
49+
return await getBase('tsconfig.json', customReadFilep, readArr, rootDir);
50+
}
51+
52+
/**
53+
* Recursively iterate through the dependency chain until we reach the end of
54+
* the dependency chain or encounter a circular reference
55+
* @param filePath Filepath of file currently being read
56+
* @param customReadFilep The file reading function being used
57+
* @param readFiles an array of the previously read files so we can check for
58+
* circular references
59+
* returns a ConfigFile object containing the data from all the dependencies
60+
*/
61+
async function getBase(
62+
filePath: string, customReadFilep: ReadFileP, readFiles: Set<string>,
63+
currentDir: string): Promise<ConfigFile> {
64+
customReadFilep = customReadFilep || readFilep;
65+
66+
filePath = path.resolve(currentDir, filePath);
67+
68+
// An error is thrown if there is a circular reference as specified by the
69+
// TypeScript doc
70+
if (readFiles.has(filePath)) {
71+
throw new Error(`Circular reference in ${filePath}`);
72+
}
73+
readFiles.add(filePath);
74+
try {
75+
const json = await customReadFilep(filePath, 'utf8');
76+
let contents = JSON.parse(json);
77+
78+
if (contents.extends) {
79+
const nextFile = await getBase(
80+
contents.extends, customReadFilep, readFiles, path.dirname(filePath));
81+
contents = combineTSConfig(nextFile, contents);
82+
}
83+
84+
return contents;
85+
} catch (err) {
86+
throw new Error(`${filePath} Not Found`);
87+
}
88+
}
89+
90+
/**
91+
* Takes in 2 config files
92+
* @param base is loaded first
93+
* @param inherited is then loaded and overwrites base
94+
*/
95+
function combineTSConfig(base: ConfigFile, inherited: ConfigFile): ConfigFile {
96+
const result: ConfigFile = {compilerOptions: {}};
97+
98+
Object.assign(result, base, inherited);
99+
Object.assign(
100+
result.compilerOptions, base.compilerOptions, inherited.compilerOptions);
101+
delete result.extends;
102+
return result;
103+
}
104+
105+
/**
106+
* An interface containing the top level data fields present in Config Files
107+
*/
108+
export interface ConfigFile {
109+
files?: string[];
110+
compilerOptions?: {};
111+
include?: string[];
112+
exclude?: string[];
113+
extends?: string[];
49114
}

test/test-util.ts

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,128 @@
1616
import test from 'ava';
1717
import * as path from 'path';
1818

19-
import {getTSConfig} from '../src/util';
19+
import {ConfigFile, getTSConfig} from '../src/util';
20+
21+
/**
22+
* Creates a fake promisified readFile function from a map
23+
* @param myMap contains a filepath as the key and a ConfigFile object as the
24+
* value.
25+
* The returned function has the same interface as fs.readFile
26+
*/
27+
function createFakeReadFilep(myMap: Map<string, ConfigFile>) {
28+
return (configPath: string) => {
29+
const configFile = myMap.get(configPath);
30+
if (configFile) {
31+
return Promise.resolve(JSON.stringify(configFile));
32+
} else {
33+
return Promise.reject(`${configPath} Not Found`);
34+
}
35+
};
36+
}
2037

2138
test('get should parse the correct tsconfig file', async t => {
2239
const FAKE_DIRECTORY = '/some/fake/directory';
23-
const FAKE_CONFIG = {a: 'b'};
40+
const FAKE_CONFIG1 = {files: ['b']};
41+
2442
function fakeReadFilep(
2543
configPath: string, encoding: string): Promise<string> {
2644
t.is(configPath, path.join(FAKE_DIRECTORY, 'tsconfig.json'));
2745
t.is(encoding, 'utf8');
28-
return Promise.resolve(JSON.stringify(FAKE_CONFIG));
46+
return Promise.resolve(JSON.stringify(FAKE_CONFIG1));
2947
}
3048
const contents = await getTSConfig(FAKE_DIRECTORY, fakeReadFilep);
31-
t.deepEqual(contents, FAKE_CONFIG);
49+
t.deepEqual(contents, FAKE_CONFIG1);
3250
});
3351

52+
test('should throw an error if it finds a circular reference', async t => {
53+
const FAKE_DIRECTORY = '/some/fake/directory';
54+
const FAKE_CONFIG1 = {files: ['b'], extends: 'FAKE_CONFIG2'};
55+
const FAKE_CONFIG2 = {extends: 'FAKE_CONFIG3'};
56+
const FAKE_CONFIG3 = {extends: 'tsconfig.json'};
57+
const myMap = new Map();
58+
myMap.set('/some/fake/directory/tsconfig.json', FAKE_CONFIG1);
59+
myMap.set('/some/fake/directory/FAKE_CONFIG2', FAKE_CONFIG2);
60+
myMap.set('/some/fake/directory/FAKE_CONFIG3', FAKE_CONFIG3);
61+
62+
63+
await t.throws(
64+
getTSConfig(FAKE_DIRECTORY, createFakeReadFilep(myMap)), Error,
65+
'Circular Reference Detected');
66+
});
67+
68+
test('should follow dependency chain caused by extends files', async t => {
69+
const FAKE_DIRECTORY = '/some/fake/directory';
70+
const FAKE_CONFIG1 = {
71+
compilerOptions: {a: 'n'},
72+
files: ['b'],
73+
extends: 'FAKE_CONFIG2'
74+
};
75+
const FAKE_CONFIG2 = {include: ['/stuff/*'], extends: 'FAKE_CONFIG3'};
76+
const FAKE_CONFIG3 = {exclude: ['doesnt/look/like/anything/to/me']};
77+
const combinedConfig = {
78+
compilerOptions: {a: 'n'},
79+
files: ['b'],
80+
include: ['/stuff/*'],
81+
exclude: ['doesnt/look/like/anything/to/me']
82+
};
83+
84+
const myMap = new Map();
85+
myMap.set('/some/fake/directory/tsconfig.json', FAKE_CONFIG1);
86+
myMap.set('/some/fake/directory/FAKE_CONFIG2', FAKE_CONFIG2);
87+
myMap.set('/some/fake/directory/FAKE_CONFIG3', FAKE_CONFIG3);
88+
89+
const contents =
90+
await getTSConfig(FAKE_DIRECTORY, createFakeReadFilep(myMap));
91+
t.deepEqual(contents, combinedConfig);
92+
});
93+
94+
test(
95+
'when a file contains an extends field, the base file is loaded first then overridden by the inherited files',
96+
async t => {
97+
const FAKE_DIRECTORY = '/some/fake/directory';
98+
const FAKE_CONFIG1 = {files: ['b'], extends: 'FAKE_CONFIG2'};
99+
const FAKE_CONFIG2 = {files: ['c'], extends: 'FAKE_CONFIG3'};
100+
const FAKE_CONFIG3 = {files: ['d']};
101+
const combinedConfig = {compilerOptions: {}, files: ['b']};
102+
const myMap = new Map();
103+
myMap.set('/some/fake/directory/tsconfig.json', FAKE_CONFIG1);
104+
myMap.set('/some/fake/directory/FAKE_CONFIG2', FAKE_CONFIG2);
105+
myMap.set('/some/fake/directory/FAKE_CONFIG3', FAKE_CONFIG3);
106+
107+
const contents =
108+
await getTSConfig(FAKE_DIRECTORY, createFakeReadFilep(myMap));
109+
t.deepEqual(contents, combinedConfig);
110+
});
111+
112+
test(
113+
'when reading a file, all filepaths should be relative to the config file currently being read',
114+
async t => {
115+
const FAKE_DIRECTORY = '/some/fake/directory';
116+
const FAKE_CONFIG1 = {files: ['b'], extends: './foo/FAKE_CONFIG2'};
117+
const FAKE_CONFIG2 = {include: ['c'], extends: './bar/FAKE_CONFIG3'};
118+
const FAKE_CONFIG3 = {exclude: ['d']};
119+
const combinedConfig =
120+
{compilerOptions: {}, exclude: ['d'], files: ['b'], include: ['c']};
121+
const myMap = new Map();
122+
myMap.set('/some/fake/directory/tsconfig.json', FAKE_CONFIG1);
123+
myMap.set('/some/fake/directory/foo/FAKE_CONFIG2', FAKE_CONFIG2);
124+
myMap.set('/some/fake/directory/foo/bar/FAKE_CONFIG3', FAKE_CONFIG3);
125+
126+
const contents =
127+
await getTSConfig(FAKE_DIRECTORY, createFakeReadFilep(myMap));
128+
t.deepEqual(contents, combinedConfig);
129+
});
130+
131+
test(
132+
'function throws an error when reading a file that does not exist',
133+
async t => {
134+
const FAKE_DIRECTORY = '/some/fake/directory';
135+
const myMap = new Map();
136+
137+
await t.throws(
138+
getTSConfig(FAKE_DIRECTORY, createFakeReadFilep(myMap)), Error,
139+
`${FAKE_DIRECTORY}/tsconfig.json Not Found`);
140+
});
141+
142+
34143
// TODO: test errors in readFile, JSON.parse.

0 commit comments

Comments
 (0)