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

fix: allow 'gts clean' to follow tsconfig.json dependency chain #185

Merged
merged 38 commits into from Jul 17, 2018

Conversation

jsamar17
Copy link
Contributor

@jsamar17 jsamar17 commented Jul 9, 2018

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

@kjin kjin self-requested a review July 9, 2018 18:12
@jsamar17 jsamar17 changed the title Issue64 fix: Allowed 'gts clean' to follow tsconfig.json dependency chain Jul 9, 2018
Copy link
Contributor

@kjin kjin left a 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;

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

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';
Copy link
Contributor

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.

Copy link
Contributor Author

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']) {
Copy link
Contributor

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'].

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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).

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor

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.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@kjin kjin left a 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 Show resolved Hide resolved
src/util.ts Outdated
await getExtension(contents['extends'], customReadFilep, readFiles);
contents = combineTSConfig(nextFile, contents);
}
// console.log(contents);
Copy link
Contributor

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? 😄

Copy link
Contributor Author

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?
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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(
Copy link
Contributor

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.

Copy link
Contributor Author

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",
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -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
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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'};
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

files: ['b'],
extends: 'CONFIG1'
};
const CONFIG1 = {include: ['/stuff/*'], extends: 'CONFIG2'};
Copy link
Contributor

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?




// TODO: test for circular reference error with symlinks
Copy link
Contributor

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-io
Copy link

codecov-io commented Jul 11, 2018

Codecov Report

Merging #185 into master will increase coverage by 0.15%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
src/util.ts 100% <100%> (ø) ⬆️
test/test-util.ts 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f7c75ee...380a100. Read the comment docs.

src/util.ts Outdated
import * as path from 'path';
import pify from 'pify';
import rimraf from 'rimraf';
import {file} from 'tmp';
Copy link
Contributor

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(...)

Copy link
Contributor Author

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)]);
Copy link
Contributor

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
*/

Copy link
Contributor

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
}
Copy link
Contributor

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
Copy link
Contributor

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': {}};
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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;

Copy link
Contributor

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 = [''];
Copy link
Contributor

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.

@@ -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 => {
Copy link
Contributor

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?

Copy link
Contributor Author

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(
'When a file, all filepaths should be relative to the config file currently being read',
Copy link
Contributor

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?

Copy link
Contributor Author

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(
'when a file contains an extends field, the base file is loaded first then overridden by the inherited files',
Copy link
Contributor

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)

Copy link
Contributor Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, makes sense.

@kjin
Copy link
Contributor

kjin commented Jul 13, 2018

cc/ @jplaisted

Copy link
Contributor

@kjin kjin left a 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}');
Copy link
Contributor

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': {}};
Copy link
Contributor

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
Copy link
Contributor

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.

configPath: string, encoding: string): Promise<string> {
switch (configPath) {
case '/some/fake/directory/FAKE_CONFIG1':

Copy link
Contributor

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)

Copy link
Contributor Author

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(
'When reading a file, all filepaths should be relative to the config file currently being read',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Make this lowercase.

@kjin kjin changed the title fix: Allowed 'gts clean' to follow tsconfig.json dependency chain fix: allow 'gts clean' to follow tsconfig.json dependency chain Jul 16, 2018
Copy link
Contributor

@kjin kjin left a 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.

import {getTSConfig} from '../src/util';
import {ConfigFile, getTSConfig} from '../src/util';


Copy link
Contributor

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.

* @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
Copy link
Contributor

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.

Copy link
Contributor

@kjin kjin left a 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 👍

@kjin kjin merged commit d0d430d into google:master Jul 17, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

gts clean doesn't follow tsconfig.json dependency chain
4 participants