-
Notifications
You must be signed in to change notification settings - Fork 23
Implement feature request #189 (Option to use global tslint instance) #196
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.
Can you also rebase this on master
? I merged in #195 so the linting configuration is up to date now.
@@ -19,33 +19,53 @@ | |||
"rulesDirectory": { | |||
"type": "string", | |||
"title": "Custom rules directory (absolute path)", | |||
"default": "" | |||
"default": "", | |||
"order": 2 |
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 you're going to add order
to these, re-order them in package.json
itself to match the order
😉.
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.
Hah, I don't know why I didn't just do that to begin with. I guess I was a bit shy contributing to a new repo and trying to keep my "lines modified" count down. 😝 It's fixed!
package.json
Outdated
}, | ||
"useGlobalTslint": { | ||
"type": "boolean", | ||
"title": "Use the global tslint install if there is no local version (or it is disabled)", |
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 should be short and sweet: Use the global TSLint install
. Combine the rest into the description.
Alright, I'll make those tweaks tonight, then rebase and update the PR. 👍 |
Okay, changes made and squashed into the original commit! Should be in good shape now. 😄 |
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.
Looks good so far, I have just a few more changes for you though =]
lib/worker.js
Outdated
@@ -93,6 +95,22 @@ async function getLocalLinter(basedir) { | |||
}); | |||
} | |||
|
|||
async function getNodePrefixPath() { |
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 function is marked async
, but I don't see any async calls
lib/worker.js
Outdated
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; | ||
let nodePrefixPath; | ||
try { | ||
nodePrefixPath = ChildProcess.spawnSync(npmCommand, ['get', 'prefix'], { |
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 shouldn't use the *Sync
versions of system commands, this takes processing time away from other atom packages due to us blocking on io
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.
Whoops, my mistake! In my haste to port over linter-eslint
's implementation, I forgot to adjust it for the fact that the rest of the code here is all nice and async'd. Didn't catch that they were using spawnSync()
, although in retrospect it should have been obvious. xD I'll get right on that. 😅
package.json
Outdated
"globalNodePath": { | ||
"type": "string", | ||
"title": "Global node installation path", | ||
"description": "The location of your global npm install (`npm get prefix`). Optional.", |
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.
Maybe add (will default to `npm get prefix`)
Alright, I went back and looked over
I also went through with the Developer Tools up and ensured that it was using the correct linter, caching correctly, etc. and I'm now confident that it's working the way I intended. 👍 |
lib/worker.js
Outdated
@@ -86,13 +89,30 @@ async function getLocalLinter(basedir) { | |||
} else { | |||
linter = tslintDef; | |||
} | |||
tslintCache.set(basedir, linter); | |||
tslintCache.set(fileDir, linter); |
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.
linterDir
?
lib/worker.js
Outdated
{ env: Object.assign(Object.assign({}, process.env), { PATH: getPath() }) }, | ||
(err, stdout, stderr) => { | ||
if (err || stderr) { | ||
throw (err || new Error(stderr)); |
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 haven't tried this before, does this bubble up correctly as if you used the reject of the promise?
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.
Yeah, I juggled with doing it this way vs. using reject()
and opted for this mainly because the try/catch in getLinter()
looked cleaner to me than the alternative "two-callbacks" method. I suppose it's really personal preference and I'll defer to you guys if you have a preference one way or the other. It does work, though.
If an error is thrown in the executor function, the promise is rejected. The return value of the executor is ignored.
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.
That's great! I totally agree it looks cleaner =]. I'll be adopting this style from now on I think.
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.
Wait, just looked at it a little closer, the .exec()
callback isn't guaranteed to be in the executor function though right? It may have a different call stack.
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.
Y'know, you're right. It would normally work, but the fact that it's taking place in another process in this case means that needs to be a Promise.catch()
rather than a regular try/catch
block. I just tested it by changing npm
to nom
and with that change made, now it does catch the rejection properly. 👍 Will update in a second.
lib/worker.js
Outdated
return resolveAndCacheLinter(basedir); | ||
} | ||
|
||
if (config.useGlobalTslint) { |
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 basedir
really used when using global tslint?
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.
Currently, yes, although I suppose there may be a slightly more efficient way to do it? getLinter()
is called with the path of the file being linted, and returns the instance of tslint
that it's supposed to use for files in that directory, which depends on whether that directory has a local tslint
package installed as a dependency. Since the local tslint
(if there is one) still takes precedence over the global install, we cache the global tslint
only for directories that we've confirmed don't have a local install. That way when directories we haven't checked yet show up, we still go through the check for the local install before going with the global or default 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.
It would also be an option to separately cache the global linter after the first time we fetch it, to avoid having to resolve it again for each new directory, but ultimately it would need to be inserted into the tslintCache
Map for each directory that we've confirmed doesn't have a local install, just like on line 109 where the same is done for the packaged "fallback" install.
Heh, so this has turned into more changes and refactoring than I intended, but:
|
lib/worker.js
Outdated
} | ||
|
||
async function getLocalLinter(basedir) { | ||
async function resolveAndCacheLinter(fileDir, moduleDir) { |
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 doesn't need to be marked as async
if no await
s are done within it
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 this is my last comment =]. @Arcanemagus, any objections after 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.
Ah, true, since all it's doing is immediately returning a Promise, it doesn't really need to be marked as async. I'll go ahead and clean up those unnecessary async
declarations on the functions that do that.
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.
Just as a note, I personally leave the async
on there just to make it obvious to myself that the function needs to be await
ed on where it gets called, not a huge issue though as you are perfectly
right that it isn't needed 😉.
lib/worker.js
Outdated
@@ -84,30 +80,78 @@ async function getLocalLinter(basedir) { | |||
linter = require('loophole').allowUnsafeNewFunction(() => require(linterPath).Linter); | |||
} | |||
} else { | |||
linter = tslintDef; | |||
return resolve(); |
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.
🐛 You should just need to call resolve()
, I'm not actually sure what return resolve()
would do, I'm guessing that resolve()
returns some value that makes that make sense as I've seen that elsewhere without issues, but the return
isn't needed.
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 noticed this too. There shouldn't be I'll effect, but for consistency I agree we should remove it
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.
Good call. I was doing it (and I suspect others do it) in order to kick execution back up the stack immediately, hence avoiding the caching operation on line 85... but that doesn't really need to be outside of the if-block anyway, so moving that inside and then getting rid of the else
seems like a cleaner option.
lib/worker.js
Outdated
} | ||
tslintCache.set(basedir, linter); | ||
tslintCache.set(fileDir, linter); |
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.
❓ Looks like @Xapphire13's earlier comment got marked resolved without being resolved:
Shouldn't this be baseDir
?
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.
Actually, that's correct as-is (at least, it's what I intended, "correct" is rather subjective 😛.) It specifically needs to be fileDir because the lookup on lines 110-113 searches the cache for the linter that was used the last time a source file in that directory was linted. If we use baseDir, which might actually be the user's npm
directory, then it'll look up which linter to use every time and it will never actually get cached in a way that it can find 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.
Ah ha, makes perfect sense. 👍
|
||
async function getLocalLinter(basedir) { | ||
function resolveAndCacheLinter(fileDir, moduleDir) { | ||
const basedir = moduleDir || fileDir; |
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.
🐛 baseDir
please 😉
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 wasn't sure how you wanted me to deal with that, since it previously was called basedir
, apparently for the sake of brevity on line 71. I guess changing that line to { basedir: baseDir }
wouldn't be the end of the world.
lib/worker.js
Outdated
return resolve(linter); | ||
}, | ||
); | ||
}); | ||
} | ||
|
||
function getNodePrefixPath() { | ||
return new Promise((fulfill, reject) => { |
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 is extrememely unlikely to change during runtime, cache it after the first (successful) call and return that.
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.
❓ Any particular reason you used fulfill
instead of the standard resolve
? I wouldn't block over it, but the non-standard nature of it requires a double take on the code.
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 npm prefix doesn't actually have to be cached because this function is never called more than once. If it ever gets to the point that it needs to grab the prefix (because there was no local linter, and the user set the "use global tslint" option but didn't explicitly give their prefix in the settings) then it immediately uses the prefix to grab the global linter. If it's able to resolve it, then it caches that one as the "fallback linter," and if it's not able to resolve it, it caches linter-tslint's version as the "fallback linter." Either way, this function should never be called again, because getLinter()
will never get past line 124 once fallbackLinter
is set. 👍
As for the fulfill/resolve thing, I'm not actually sure... I thought maybe I had seen it in linter-eslint or something and copied the style, but now I can't find it anywhere. Dunno where I got it from, to be honest. xD I'll just change it to resolve
, I don't really care either way and resolve does seem more "standard," as you said.
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.
Makes sense about the calling of getNodePrefixPath
, thanks for clearing that up!
resolve
is actually part of the standard documentation (Promise), but you can use whatever name you want... almost everyone just uses the standard names though lol.
lib/worker.js
Outdated
} | ||
|
||
const globalLint = await getNodePrefixPath() | ||
.then(prefix => resolveAndCacheLinter(basedir, prefix)) |
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 this using Promise
syntax? Nothing in here is a callback function that requires a Promise
wrapper to work...
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 guess the .then()
isn't 100% necessary, I just figured it was easier to chain the two promises and await
them together, but since resolveAndCacheLinter()
never rejects, I guess it doesn't really have to be before the catch()
. It could also be done like:
const prefix = await getNodePrefixPath().catch((err) => {
...
});
if (prefix) {
const globalLint = await resolveAndCacheLinter(basedir, prefix);
fallbackLinter = globalLint;
return globalLint;
}
I guess it's just preference as to which makes the flow more obvious? I'd be okay with either.
lib/worker.js
Outdated
The error message encountered was:\n\n${err.message}`); | ||
}); | ||
if (globalLint) { | ||
fallbackLinter = globalLint; |
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.
❓ Should this be cached in tslintCache
?
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 probably leave fallbackLinter
in its own variable, personally? I mean, you could put it into the map with everything else, but since it could technically be the global linter or the linter-tslint packaged version, that sorta makes finding it again a little complicated. This way, it's just "No local linter? Alright, use the other one." and you're done.
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.
Makes sense. 👍
lib/worker.js
Outdated
} | ||
|
||
async function getLocalLinter(basedir) { | ||
async function resolveAndCacheLinter(fileDir, moduleDir) { |
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.
Just as a note, I personally leave the async
on there just to make it obvious to myself that the function needs to be await
ed on where it gets called, not a huge issue though as you are perfectly
right that it isn't needed 😉.
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.
Here's what I see, key for the comments:
🐛 Definitely looks like something is wrong
🐎 Nothing wrong, but it should be changed for performance
❓ Maybe something wrong, or just a question/note.
Looks like this is getting close though!
Well that's annoying, it posted all my pending review things when I made the single comment here 😞. |
@Arcanemagus, GitHub won't let me response to your comment about This begs the question, should this project be rewritten in TypeScript? =] |
Fascinating, I wonder if that's an artifact of TypeScript's method of transpilation to ES5? It's not necessarily a bug in JavaScript, it's technically a bit of overhead though as initializing an In any case, like I said it's just a handy notation, it's not important so leaving it out is just fine.
I'm not against it, I have no experience with it though 😛. |
lib/worker.js
Outdated
return new Promise((resolve) => { | ||
if (!requireResolve) { | ||
requireResolve = require('resolve'); | ||
} | ||
requireResolve( | ||
tslintModuleName, | ||
{ basedir }, | ||
{ basedir: baseDir }, |
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.
Oh, didn't see that it was being sent as a parameter here. I guess we should probably keep this as basedir
then. Sorry!
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.
@smhxx Oh, this was supposed to be in the requested changes below.
lib/worker.js
Outdated
} | ||
tslintCache.set(basedir, linter); | ||
tslintCache.set(fileDir, linter); |
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 ha, makes perfect sense. 👍
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.
One last little thing!
lib/worker.js
Outdated
} | ||
} | ||
|
||
const prefix = await getNodePrefixPath().catch((err) => { |
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 should be in a try
/catch
block:
let prefix
try {
prefix = await getNodePrefixPath();
} catch (err) {
// eslint-disable-next-line no-console
console.warn(`Attempted to load global tslint, but \`npm get prefix\`
failed. Falling back to the packaged version of tslint. You can specify
your prefix manually in the settings or linter-tslint config file.\n
The error message encountered was:\n\n${err.message}`);
}
That's what I meant as part of the "Promise
syntax", sorry for not making that clear 😉.
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! The .catch()
. 😛 Now I understand. Should be easy since that the two promises are separated already.
Sorry about being all over the place with the reviews, apparently I can't remember comments post pending review comments tonight. |
As for TypeScript, the compiler will complain if you have an Going from JavaScript -> TypeScript is pretty easy if you aren't doing any wonky wtf-level polymorphism that would make Douglas Crockford cry. Making the jump probably wouldn't be as bad as it sounds, although I suppose the question is "why," if it's just gonna have to be compiled back into JS for |
Type checking is definitely a good thing, with |
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.
Last bit that I see
lib/worker.js
Outdated
return new Promise((resolve) => { | ||
if (!requireResolve) { | ||
requireResolve = require('resolve'); | ||
} | ||
requireResolve( | ||
tslintModuleName, | ||
{ basedir }, | ||
{ basedir: baseDir }, |
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.
Looks like this got lost in the mess of comments, since this name needs to be sent as an object into requireResolve
, let's just keep it as basedir
in the entire function. Sorry for going back and forth on 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.
woops! It's okay, I'll change it back. haha
Added new option to config & settings to use the global installation of tslint as a fallback, rather than the version included with linter-tslint.
I was wrong, I think this may just be a C#-ism (which returns
I would say for type checking and features that JS (and ES6/7) doesn't have yet. It may also yield a slight perf. increase since it would no longer be transpiled ES6+ running through babel at runtime. @Arcanemagus, we could get the TypeScript to compile upon install using |
I bet you could even do it as a |
Looks like |
Atom has a tl;dr it only transpiles files once 😉.
Not quite (for the reason @smhxx found). If you want to use transpiled code you either have to store both the source and transpiled version in the repository... or use the |
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, thanks for sticking with this! ❤️
@Arcanemagus Surprisingly, I looked and wasn't able to find one anywhere. Really surprises me since I'd imagine plenty of people are probably interested in being able to use TS to write Atom packages. ~70 LOC later, the problem has been resolved. 😉 EDIT: Of course, now the problem is that |
@smhxx, awesome job! I was thinking about making something similar myself =]. |
Btw, Atom actually has native support for |
Howdy, all! 👋
I stumbled across #189 earlier today after wishing for the exact same feature... it's fairly simple and
linter-eslint
already has a similar option, so I decided it was worth wrangling their implementation intolinter-tslint
as well.There were some minor linting issues with the current codebase due to recent versions of eslint (you can see what I'm talking about in the CI report for #195) so I took the initiative to resolve those as well. Two of them were super minor and well-founded, but the other one introduced by v4.6.0 was a bit silly (as others have mentioned,) so I went ahead and // eslint-disabled the relevant lines to avoid mucking the style up without a second opinion. Anyway, I hope this is helpful, and any feedback is of course welcome. 😃 👍