-
-
Notifications
You must be signed in to change notification settings - Fork 95
Prefer local prettier over bundled version #104
Conversation
Codecov Report
@@ Coverage Diff @@
## master #104 +/- ##
==========================================
+ Coverage 78.84% 80.12% +1.27%
==========================================
Files 6 6
Lines 156 166 +10
==========================================
+ Hits 123 133 +10
Misses 33 33
Continue to review full report at Codecov.
|
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 great stuff! Don't forget to add yourself to the contributors list (check nps)!
I left some minor style comments, but the code LGTM. Should we leave the option (whether it should fallback or not) for a new PR and merge this?
Calling prettier through the CLI works fine, but does introduce a short but noticeable lag (~300 ms on a MacBook Pro, so probably even more significant elsewhere). This is mentioned as a potential issue in prettier/prettier#918.
Are you referring to the first time you call Prettier in Atom (this lag already happens even before this PR), or does this happen every time now?
@robwise Is this a significant enough performance penalty to go back to dynamic require until jsonrpc or something similar is introduced in prettier?
Hmm, good question here. If it's just the initial/first time, then I think it's fine. If it's every time, I'm not as sure, 300ms sounds like a lot in some contexts, in others it's not even detectable. I'm not really sure on this.
src/helpers.js
Outdated
const getPrettierCliOptions = (options: Object) => | ||
Object.keys(options) | ||
.reduce((cliOptions, option) => [...cliOptions, `${CLI_OPTIONS[option]} ${options[option]}`], []) | ||
.join(' '); |
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 seems like you're using a reduce
to imitate a map
? I think you could just use a map directly:
const getPrettierCliOptions = (prettierOptions: {}) =>
Object.keys(prettierOptions).map(option => `${CLI_OPTIONS[option]} ${prettierOptions[option]`).join(' ');
or you could stay with reduce and skip the array/join:
const getPrettierCliOptions = (prettierOptions: {}) =>
Object.keys(prettierOptions).reduce((acc, option) => `${acc} ${CLI_OPTIONS[option]} ${prettierOptions[option]`);
Also, I think we maybe should just take the editor
as an argument and call prettierOptions
ourselves instead of making the caller do it, that way the caller is less coupled:
const getPrettierCliOptions = (editor: TextEditor) =>
Object.keys(getPrettierOptions(editor)).reduce((acc, option) => `${acc} ${CLI_OPTIONS[option]} ${prettierOptions[option]`);
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.
Ohh 🤦♂️ this is what happens when I write code in the evening. This used to concatenate strings like your second suggestion, that's why reduce
, then I changed my mind to do an array and a join and didn't notice it's now just a map.
Happy to change it to take editor
instead of options.
src/helpers.test.js
Outdated
|
||
test('translates complex options', () => { | ||
const expected = '--single-quote true --print-width 100 --trailing-comma es5'; | ||
const actual = getPrettierCliOptions({ singleQuote: true, printWidth: 100, trailingComma: 'es5' }); |
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 nit-picky, but can we reverse the order of expected
and actual
to match 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.
Sure.
src/helpers.test.js
Outdated
const actual = getPrettierCliOptions({}); | ||
|
||
expect(actual).toEqual(expected); | ||
}); |
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 don't think this case should ever occur, should it? We're always going to retrieve all options, 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.
Happy to remove that, that's a relic of my test-driving the implementation :)
I just read about this in the prettier-atom RFC for linter integration. I'm not totally sure I understand all of this properly. |
I thought about this some more over night and I think we could solve the performance problem by having the I also realised I haven't done any error handling for the CLI call. Added it in the to do list at the top. |
It is every time now. I'm not very happy with it as is.
Up to you, happy to do either. |
Yeah, now that I think of it, it may be very difficult to parse the stack trace/error that comes out of the CLI. When you combine that with the performance hit and the complication of needing to run it in the background (which is sort of begging for memory bloat if not leaks IMO), maybe it's better to go with programatically invoking it instead of using the CLI? |
In that case, can you do a separate PR when you get to that step? That way the PRs are nice and tight! BTW, anytime you want to pair or just if you feel like it's too much work and want to hand it off, let me know, I really appreciate all of this help. |
I think running it as a child process, while it requires some work, will work best - no dynamic requires, no startup penalty and we can make sure to only ever have one instance running (closing the previous one if there is one). You're right the error output could be difficult to parse, but as it stands, I don't think the error reporting is parsing the message at all, does it? It's just printed into an alert. I'll give it a couple more hours tonight and if it gets crazy, lets go with a dynamic
Sure, will open as a separate PR. |
Sounds good to me! |
I looked into whether the streaming will work and I don't think it will without changes to prettier itself - it currently waits for So that settles it - it's got to be a dynamic |
@charypar Should I pick this one up? |
@robwise Oh, thanks for the reminder. Past couple weeks were busy, but I've got time tonight, I'll make the changes and push them in a couple hours. |
To make prettier-atom work nicer in projects with prettier installed, it will now prefer the local version of the prettier package over the bundled dependency, to give output consistent with package scripts and CLI prettier. If no local prettier can be found, we fallback to the bundled package.
7b8c0b1
to
98e2ea9
Compare
@robwise Ok, I think this is ready to merge, if you're happy with it. Special error handling is no longer needed, errors can be handled the same way for both bundled and local prettier. Speaking of errors, would you be interested in a PR that would show parse errors from prettier inline using |
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 awesome! I just had a couple of code style comments/requests.
src/executePrettier.js
Outdated
return prettier.format(text, prettierOptions); | ||
} | ||
|
||
return getLocalPrettier(localPrettier).format(text, prettierOptions); |
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.
Code style comment: I think there's more than one responsibility going on in this function. This function is supposed to be responsible for executing prettier, but now it also has the responsibility for figuring out which prettier to use and where to get it.
I think it would be better, therefore, if we made a helper method simply called getPrettier
that would handle all of this internally. This would also make mocking much easier because we can just mock getPrettier
.
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.
Sure, that makes more sense, we can then just past the prettier instance to use to the executePrettier
function.
We could even pass prettier down the execute stack from format
/ formatOnSave
, like the editor
.
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 don't know if I'd do the format
thing, then you're putting in the logic in two different places that otherwise had no need to know anything about how executePrettier
works?
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 could move it all the way up to main
to do it in one place and pass it through both format
functions, I suppose?
Although I guess we might as well hide the problem in the helpers
by wrapping it in one function :)
src/helpers.js
Outdated
|
||
const indexPath = path.join('node_modules', 'prettier', 'index.js'); | ||
const dirPath = getDirFromFilePath(filePath); | ||
if (!dirPath) return null; |
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 guard clause possible to hit? If not, you can go FP and avoid state:
const getLocalPrettierPath = (filePath: ?FilePath): ?FilePath =>
filePath ?
findCached(getDirFromFilePath(filePath), path.join('node_modules', 'prettier', 'index.js'))
: null;
Also, see my comment in executePrettier.js
. There's really no reason to not just give the prettier
instance itself instead of making executePrettier
do 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.
Yep, makes sense, ternary it is. The reason to split the path/lookup logic from the actual requiring is it makes it easier to test in isolation.
src/helpers.test.js
Outdated
const expectedLib = path.join('node_modules', 'prettier', 'index.js'); | ||
|
||
expect(atomLinter.findCached).toHaveBeenCalledWith(expectedDir, expectedLib); | ||
}); |
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 test seems to pretty much be checking the same thing as the one above? Should we just keep 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.
One is checking we get the right thing back given findCached
returns it, the other checks we call findCached
with the right arguments.
I had them in one test at first, but this way they can fail independently. Happy to merge them together if you'd prefer.
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 is checking we get the right thing back given findCached returns it, the other checks we call findCached with the right arguments.
Yeah although I'd argue the first one is what we need the function to do, and the second one is just an implementation detail, so I think we can drop that one.
Yeah definitely, that would be awesome. |
I'll make those changes tonight, should be quick. |
3b77be4
to
4d21fb2
Compare
@robwise This should address all the review comments. Please let me know if you want to change anything else. |
Move the logic of loading prettier (either local or global) to the helpers.js to simplify executePrettier.
4d21fb2
to
26af8d4
Compare
Awesome! Thanks! |
Following on the conversation in #59, this is the first cut of the implementation.
Calling prettier through the CLI works fine, but does introduce a short but noticeable lag (~300 ms on a MacBook Pro, so probably even more significant elsewhere). This is mentioned as a potential issue in prettier/prettier#918.
@robwise Is this a significant enough performance penalty to go back to dynamic require until jsonrpc or something similar is introduced in prettier?
I still need to implement the configuration option requested in #43.
to do
Add configuration to only use local prettier and do nothing if there isn't one