Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Prefer local prettier over bundled version #104

Merged
merged 3 commits into from
Apr 12, 2017

Conversation

charypar
Copy link
Contributor

@charypar charypar commented Mar 27, 2017

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

  • Prefer local prettier over bundled version
  • Error handling
  • Add configuration to only use local prettier and do nothing if there isn't one

@codecov-io
Copy link

codecov-io commented Mar 27, 2017

Codecov Report

Merging #104 into master will increase coverage by 1.27%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
src/executePrettier.js 100% <100%> (ø) ⬆️
src/helpers.js 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 a38ea92...26af8d4. Read the comment docs.

Copy link
Collaborator

@robwise robwise left a 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(' ');
Copy link
Collaborator

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]`);

Copy link
Contributor Author

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.


test('translates complex options', () => {
const expected = '--single-quote true --print-width 100 --trailing-comma es5';
const actual = getPrettierCliOptions({ singleQuote: true, printWidth: 100, trailingComma: 'es5' });
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure.

const actual = getPrettierCliOptions({});

expect(actual).toEqual(expected);
});
Copy link
Collaborator

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?

Copy link
Contributor Author

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

@robwise
Copy link
Collaborator

robwise commented Mar 28, 2017

I just read about this in the prettier-atom RFC for linter integration. I'm not totally sure I understand all of this properly.

@charypar
Copy link
Contributor Author

I thought about this some more over night and I think we could solve the performance problem by having the prettier process running in the background and streaming input/output. That way, we'd only pay the penalty once when starting the process and from then on it would be quick. If I imagine it correctly that is.

I also realised I haven't done any error handling for the CLI call. Added it in the to do list at the top.

@charypar
Copy link
Contributor Author

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.

It is every time now. I'm not very happy with it as is.

Should we leave the option (whether it should fallback or not) for a new PR and merge this?

Up to you, happy to do either.

@robwise
Copy link
Collaborator

robwise commented Mar 28, 2017

I also realised I haven't done any error handling for the CLI call. Added it in the to do list at the top.

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?

@robwise
Copy link
Collaborator

robwise commented Mar 28, 2017

Up to you, happy to do either.

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.

@charypar
Copy link
Contributor Author

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?

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 require. How does that sound?

In that case, can you do a separate PR when you get to that step?

Sure, will open as a separate PR.

@robwise
Copy link
Collaborator

robwise commented Mar 28, 2017

Sounds good to me!

@charypar
Copy link
Contributor Author

I looked into whether the streaming will work and I don't think it will without changes to prettier itself - it currently waits for stdin to close before attempting to format and returning any output.

So that settles it - it's got to be a dynamic require based on how eslint / prettier-eslint does things. Oh well :)

@robwise
Copy link
Collaborator

robwise commented Apr 10, 2017

@charypar Should I pick this one up?

@charypar
Copy link
Contributor Author

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

@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 atom-linter, like eslint does?

Copy link
Collaborator

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

return prettier.format(text, prettierOptions);
}

return getLocalPrettier(localPrettier).format(text, prettierOptions);
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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?

Copy link
Contributor Author

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;
Copy link
Collaborator

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.

Copy link
Contributor Author

@charypar charypar Apr 11, 2017

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.

const expectedLib = path.join('node_modules', 'prettier', 'index.js');

expect(atomLinter.findCached).toHaveBeenCalledWith(expectedDir, expectedLib);
});
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Collaborator

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.

@robwise
Copy link
Collaborator

robwise commented Apr 11, 2017

Speaking of errors, would you be interested in a PR that would show parse errors from prettier inline using atom-linter, like eslint does?

Yeah definitely, that would be awesome.

@charypar
Copy link
Contributor Author

I'll make those changes tonight, should be quick.

@charypar
Copy link
Contributor Author

@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.
@robwise
Copy link
Collaborator

robwise commented Apr 12, 2017

Awesome! Thanks!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants