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
fix: allow 'gts clean' to follow tsconfig.json dependency chain #185
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments.
src/clean.ts
Outdated
@@ -30,6 +30,7 @@ export async function clean(options: Options): Promise<boolean> { | |||
const tsconfig = (await getTSConfig(options.targetRootDir)) as TSConfig; | |||
if (tsconfig.compilerOptions && tsconfig.compilerOptions.outDir) { | |||
const outDir = tsconfig.compilerOptions.outDir; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there are no logical changes to this file, can you keep the original?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm mistaken, it seems like this file wasn't reverted to the original.
src/util.ts
Outdated
@@ -15,6 +15,7 @@ | |||
*/ | |||
|
|||
import * as fs from 'fs'; | |||
import {access, read} from 'fs/promises'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to support Node 6 and up, and I think 'fs/promises'
is only available in Node 10, so I don't think we can use it.
That being said, it seems like these fields are unused, so maybe we can remove this import entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
let contents = JSON.parse(json); | ||
|
||
const readArr = ['tsconfig.json']; | ||
if (contents['extends']) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For known properties of an object, prefer x.y
over x['y']
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
@@ -38,12 +39,77 @@ export function nop() { | |||
/** | |||
* Find the tsconfig.json, read it, and return parsed contents. | |||
* @param rootDir Directory where the tsconfig.json should be found. | |||
* If the tsconfig.json file has an "extends" field hop down the dependency tree until it ends |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the /** ... */
comments specifically annotate getTSConfig
. Could you move ConfigFile
to preserve this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
*/ | ||
|
||
|
||
export interface ConfigFile { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add JSDocs for ConfigFile
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
await getExtension(contents['extends'], customReadFilep, readArr); | ||
contents = combineTSConfig(extension, contents); | ||
} | ||
return Promise.resolve(contents); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just return contents
. The return type Promise<ConfigFile>
is already guaranteed because this is an async function
(async function
always returns a Promise
, no matter what).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
filePath: string, customReadFilep: ReadFileP, | ||
readFiles: string[]): Promise<ConfigFile> { | ||
customReadFilep = customReadFilep || readFilep; | ||
if (!filePath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check isn't necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline as to why. In general, typechecking allows us to forgo the check as long as the function's not user-facing. (User in this case is anyone who imports this file.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...and some more comments. Great test cases!
src/util.ts
Outdated
await getExtension(contents['extends'], customReadFilep, readFiles); | ||
contents = combineTSConfig(nextFile, contents); | ||
} | ||
// console.log(contents); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you forget to remove this line? 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
throw new Error('Undefined passed in'); | ||
} | ||
if (readFiles.indexOf(filePath) !== -1) { | ||
// did I throw the error correctly? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep!
Maybe as part of the error message, I would suggest adding more details about the error? Like the formatted contents of readFiles
, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
// did I throw the error correctly? | ||
throw new Error('Circular Reference Detected'); | ||
} | ||
readFiles.push(filePath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filePath
may be relative to the location of tsconfig.json
. This might result in issues if say, tsconfig.json
depends on foo/tsconfig.bar.json
which depends on tsconfig.json
: does that last file refer to ./tsconfig.json
or ./foo/tsconfig.json
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
} | ||
|
||
async function getExtension( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider calling this getBase
instead, because the the config file at filePath
overrides the config that it extends, not the other way around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/util.ts
Outdated
return Promise.resolve(contents); | ||
} | ||
|
||
// the inherited config file overwrites the base config file's "files", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this comment to the lines for which it's relevant? Which, as of this version is around 110.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
test/test-util.ts
Outdated
@@ -31,4 +31,74 @@ test('get should parse the correct tsconfig file', async t => { | |||
t.deepEqual(contents, FAKE_CONFIG); | |||
}); | |||
|
|||
|
|||
|
|||
// TODO: test for error due to circular reference |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This TODO can be removed now, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
test/test-util.ts
Outdated
test('should throw an error if it finds a circular reference', async t => { | ||
const FAKE_DIRECTORY = '/some/fake/directory'; | ||
const FAKE_CONFIG = {files: ['b'], extends: 'CONFIG1'}; | ||
const CONFIG1 = {extends: 'CONFIG2'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest calling these FAKE_CONFIG2, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
test/test-util.ts
Outdated
files: ['b'], | ||
extends: 'CONFIG1' | ||
}; | ||
const CONFIG1 = {include: ['/stuff/*'], extends: 'CONFIG2'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add exclude
and compilerOptions
to demonstrate that (1) overwriting happens correctly?
test/test-util.ts
Outdated
|
||
|
||
|
||
// TODO: test for circular reference error with symlinks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't necessarily need to test this if other comments are addressed. This is because even with symlinks, we are keeping a cache of "seen" config files, and having symlinks doesn't change the fact that a file will eventually be "seen" twice if it's part of a cycle.
Codecov Report
@@ Coverage Diff @@
## master #185 +/- ##
==========================================
+ Coverage 98.37% 98.52% +0.15%
==========================================
Files 12 12
Lines 737 814 +77
Branches 61 65 +4
==========================================
+ Hits 725 802 +77
Misses 12 12
Continue to review full report at Codecov.
|
src/util.ts
Outdated
import * as path from 'path'; | ||
import pify from 'pify'; | ||
import rimraf from 'rimraf'; | ||
import {file} from 'tmp'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because file
is so broad in its meaning, I would suggest import * as tmp
so as to be more specific. tmp.file(...)
is more meaningful than file(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module isn't used in this file so I removed it
src/util.ts
Outdated
// did I throw the error correctly? | ||
throw new Error('Circular Reference Detected'); | ||
throw new Error( | ||
'Circular reference in ' + readFiles[readFiles.indexOf(filePath)]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest using the following string interpolation syntax:
const x = 'hello'
console.log(`${x}, world`) // prints "hello, world"
src/util.ts
Outdated
* An interface containing the high level data fields present in Config Files | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Remove empty line between docs for X and X itself.
src/util.ts
Outdated
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Add a line between a class/interface definition and the docs for the next definition. (I think this is also true of getBase
)
src/util.ts
Outdated
// the inherited config file overwrites the base config file's "files", | ||
// "include", and "exclude" fields and combines compiler options | ||
/** | ||
* takes in 2 config files |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you write full sentences in JSDocs? Here as well as in @param
parameter descriptions.
src/util.ts
Outdated
'exclude': [], | ||
'extends': {} | ||
}; | ||
const result = {'compilerOptions': {}, 'extends': {}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is extends
one of the fields here? It seems like it's guaranteed to be deleted later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's there for type definition purposes, I would annotate result to be of type ConfigFile
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If combineTSConfig is called that means that a file was contains an extends field. If the extends field is not removed the program will create an infinite loop. I updated it so that it is now part of the ConfigFile definition.
src/clean.ts
Outdated
@@ -30,6 +30,7 @@ export async function clean(options: Options): Promise<boolean> { | |||
const tsconfig = (await getTSConfig(options.targetRootDir)) as TSConfig; | |||
if (tsconfig.compilerOptions && tsconfig.compilerOptions.outDir) { | |||
const outDir = tsconfig.compilerOptions.outDir; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm mistaken, it seems like this file wasn't reverted to the original.
src/util.ts
Outdated
|
||
const tsconfigPath = path.join(rootDir, 'tsconfig.json'); | ||
|
||
const readArr = ['']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider making this a Set
.
test/test-util.ts
Outdated
@@ -97,8 +97,86 @@ test('should follow dependency chain caused by extends files', async t => { | |||
}); | |||
|
|||
|
|||
test('should throw an error if it finds a circular reference', async t => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the same test as above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I removed the redundant test
test/test-util.ts
Outdated
}); | ||
|
||
test( | ||
'When a file, all filepaths should be relative to the config file currently being read', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first part of this sentence seems to be missing a predicate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes, It should be reading
test/test-util.ts
Outdated
}); | ||
|
||
test( | ||
'when a file contains an extends field, the base file is loaded first then overridden by the inherited files', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it is already covered in a test above. (should follow dependency chain caused by extends files
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difference is that in the first test each config file has a different part of the data and no overwriting is happening, just combining the different fields. This test actually looks to make sure properties are being overwritten correctly. If that is redundant we can remove one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, makes sense.
cc/ @jplaisted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there!
src/util.ts
Outdated
filePath = path.resolve(currentDir, filePath); | ||
|
||
if (readFiles.has(filePath)) { | ||
throw new Error('Circular reference in ${filePath}'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For posterity, can you include a comment on why we throw here (rather than give up and return immediately)?
src/util.ts
Outdated
* @param inherited is then loaded and overwrites base | ||
*/ | ||
function combineTSConfig(base: ConfigFile, inherited: ConfigFile): ConfigFile { | ||
const result: ConfigFile = {'compilerOptions': {}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Remove ''
around compilerOptions
.
src/util.ts
Outdated
} | ||
|
||
/** | ||
* An interface containing the high level data fields present in Config Files |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: top level
is more characteristic here.
test/test-util.ts
Outdated
configPath: string, encoding: string): Promise<string> { | ||
switch (configPath) { | ||
case '/some/fake/directory/FAKE_CONFIG1': | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I missed this before. What is this case for? Is this falling through to the next case?
(Here and also in the next test)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to a symlink the file FAKE_CONFIG1 has 2 addresses /some/fake/directory/tsconfig.json and /some/fake/directory/FAKE_CONFIG1. I could edit this out and make the circular references point back to /some/fake/directory/tsconfig.json I kept this in all the tests for consistancy but I can remove it
test/test-util.ts
Outdated
}); | ||
|
||
test( | ||
'When reading a file, all filepaths should be relative to the config file currently being read', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Make this lowercase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed in person, we can use a function factory for fakeReadFilep
.
test/test-util.ts
Outdated
import {getTSConfig} from '../src/util'; | ||
import {ConfigFile, getTSConfig} from '../src/util'; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make a one line break here instead of three.
test/test-util.ts
Outdated
* @param myMap contains a filepath as the key and a ConfigFile object as the | ||
* value. The fake promisified readFile function has the same interface of | ||
* fs.readFile and takes in a | ||
* @param configPath and returns a corresponding ConfigFile object or throws an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This @param
annotation is a little misleading, since it's a parameter of the returned function, not this one. I think it's acceptable to just say that the return value has the same interface as a promisifed fs.readFile function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM when CI passes 👍
I wrote and edited the tests to show the new functionality and to catch circular references. This still should be abstracted to effect all commands that look at the contents of tsconfig.json. Also there is no support for compileOnSave yet. fix: #64