Prettier is good. So good, it’s easy to get used to it, and subsequently want to retrofit it onto existing projects. Here’s a step-by-step guide and example: configuring and documenting Prettier and reformatting existing code, all the while making sure not to break other people’s work.
The example
All the steps described below are demonstrated publicly in a real-world project: Wagtail, via issue #6059 Automatic code formatting with Prettier. The implementation was split into two PRs: #7908 Add Prettier configuration & documentation, #7912 Prettier reformatting, and finally one extra commit, 7eb8730d7372dc6b4752308a7e3354b36810f32e.
Finally, here are instructions on resolving conflicts that weren’t directly stored in the repository (as this is a one-off task).
Why two PRs and a commit
I recommend always splitting the addition of any formatting tool into at least two PRs and the extra commit:
- The first PR takes care of anything that requires careful review – configuration, integrations, documentation.
- The second PR takes care of the actual reformatting.
- Finally, the final commit is just there to finish setting up the
.git-blame-ignore-revs
file for git’signoreRevsFile
.
Configuration
This is actually the simplest step.
- Update the project’s
.editorconfig
first, as Prettier reads it (here’s an example). - Create a
prettier.config.js
with any desired options. - Create a
.prettierignore
. I’d recommend to add large folders in here to speed up Prettier looking for files when using very broad glob patterns.
Personally I’ll always prefer to set all configuration options, even when using defaults. This saves having to remember what options are available that might not be set on the particular project when re-reviewing the configuration. I also always prefer using JavaScript for configuration – syntax highlighting and auto-formatting are dependable.
Which languages should I use Prettier for?
I’d recommend all languages that Prettier supports, potentially with the exception of Markdown. Although the reformatting is excellent, enforcing a single format for documentation often feels cumbersome – particularly if the project’s documentation might often be updated by contributors with little knowledge of the project’s tooling.
If a supported language isn’t used, make sure to add its extension to the .prettierignore
so the files are consistently skipped. Here’s an example:
# Irrelevant files ignored for performance reasons.
node_modules
public/
coverage/
vendor/
static
_build
# File types which Prettier supports but we don’t want auto-formatting.
*.md
# Files which contain incompatible syntax.
*.html
wagtail/admin/templates/wagtailadmin/edit_handlers/inline_panel.js
Integrations
I’ll always start with the same two commands:
"format": "prettier --write '**/?(.)*.{css,scss,js,ts,tsx,json,yaml,yml}'",
"lint:format": "prettier --check '**/?(.)*.{css,scss,js,ts,tsx,json,yaml,yml}'"
The glob pattern might seem complicated – it’s wrapped in single quotes to prevent shell globbing (thus making it consistent cross-platform). It uses **/?(.)*
so files are checked at all directory levels in the current folder, and regardless of whether file names start with a dot or not.
As to where those commands should run – generally in continuous integration, potentially in Git pre-commit hooks.
pre-commit
With the confusingly-named pre-commit framework, it looks like:
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.5.1
hooks:
- id: prettier
types_or: [css, scss, javascript, ts, tsx, json, yaml]
Note types_or
aren’t always file extensions, they’re arbitrary identifiers for some languages.
Here’s what I personally prefer – just bash:
#!/usr/bin/env bash
# Fail on first line that fails.
set -e
# Only keep staged files that are added (A), copied (C) or modified (M).
STAGED=$(git --no-pager diff --name-only --cached --diff-filter=ACM)
# Files which are only partly staged (eg. git add --patch).
PATCH_STAGED=$(git --no-pager diff --name-only --diff-filter=ACM $STAGED)
# Files which are fully staged.
FULLY_STAGED=$(comm -23 <(echo "$STAGED") <(echo "$PATCH_STAGED"))
PRETTIER_STAGED=$(grep -E '.(css|scss|js|ts|tsx|json|yaml|yml)$' <<< "$STAGED" || true)
PRETTIER_FULLY_STAGED=$(grep -E '.(css|scss|js|ts|tsx|json|yaml|yml)$' <<< "$FULLY_STAGED" || true)
# Format and re-stage fully staged files only.
if [ -n "$PRETTIER_FULLY_STAGED" ];
then
npx prettier --write $PRETTIER_FULLY_STAGED
git add $PRETTIER_FULLY_STAGED
fi
# Lint all staged files.
if [ -n "$PRETTIER_STAGED" ];
then
npx prettier --check $PRETTIER_STAGED
fi
IDEs
VS Code has excellent Prettier support. Use it. I personally prefer to default to editor.formatOnSave
turned on, and only disable it per project that doesn’t have Prettier.
Continuous integration
We want to make sure we run npm run lint:format
on CI. Simple as that. Worth mentioning pre-commit.ci, compatible with the same pre-commit framework as above, which runs formatting on your behalf and commits the changes to PRs. I personally don’t use it, but it works.
A common gotcha when setting up Prettier in CI is if your builds are optimised to only run when specific folders are changed – by definition, the Prettier checks will have to run whenever any file supported by Prettier changes.
Documentation
Make sure your contributors are aware Prettier is in use, and that there is a npm run format
command available for cases where Prettier formatting doesn’t get automatically applied in an editor.
Git blame compatibility
GitHub makes it easy to skip a commit in the blame view. For git itself, we can use a .git-blame-ignore-revs
file. Prettier itself has an excellent example, which is self-documenting:
# git-blame ignored revisions
# To configure, run
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Requires Git > 2.23
# See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt
# Prettier bump after release
# 2.5.1
4683a6284d53b913f129694a5be666fc55b06982
# 2.5.0
846dbdd7e8c9deb315b3e663b3103c559e798a89
[…]
The initial reformatting
All of the above steps should be done in the first PR, so they can easily be code reviewed. In the second PR, we can make the actual formatting changes. Run your npm run format
now, and commit the results.
If your auto-formatting creates a lot of changes and you’ll want to review them manually – you could consider starting from a Prettier configuration that does fewer changes, commit the formatting, and add more changes gradually. This makes it easier to review different formatting changes overall.
Manual issues to fix
If your project uses per-line linting disable comments, make sure the comments still target the correct lines after the reformatting. ESLint has an excellent --report-unused-disable-directives
which helps a lot. Here’s an example where this happened:
- // eslint-disable-next-line func-names
$(
'#new a.task-type-choice, #new a.choose-different-task-type',
modal.body,
- ).on('click', function () {
+ ).on('click', function onClickNew() {
There can be other compatibility issues with other static analysis tools, although that’s very rare. For example, I had to disable Prettier for a line where a Stylelint plugin wasn’t producing the expected output anymore – I’d suspect it’s the plugin’s analysis that’s causing the actual issue:
- --#{$name}: hsl(
- #{var(--#{$name}-hue),
- var(--#{$name}-saturation),
- var(--#{$name}-lightness)}
- );
+ // Prettier causes a linting issue when reformatting this.
+ /* prettier-ignore */
+ --#{$name}: hsl(#{ var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness) });
I’ve never encountered a case where Prettier changed the semantics of the code at runtime, except perhaps for trailing commas in function parameters, which should be watched out for on projects that support legacy browsers.
After the reformatting
Merge it all, making sure CI is linting for Prettier formatting, and passes. Then edit your project’s .git-blame-ignore-revs
with the correct reformatting commit, and that’s it.
For projects with other contributors – make sure to let them know there will be conflicts, and how to resolve them. Here are instructions I made for Wagtail:
Instructions to rebase in-progress PRs
Here are the high-level steps:
- Rebase onto the commit before the reformatting (11ccf30f0044a326abef2a5c5568f373ffe46188, or can also be defined as
af942a27e41b47e257b6cd46c01a13cd381fed04^1
, note the^1
suffix), with your usual rebasing methodology. - Rebase onto the reformatting commit (af942a27e41b47e257b6cd46c01a13cd381fed04 /
af942a27e41b47e257b6cd46c01a13cd381fed04
), always favouring your branch’s changes, and auto-applying Prettier reformatting. - Rebase onto
main
with your usual rebasing methodology.
Here are step-by-step commands, tested on macOS with zsh:
# Make a copy of your branch
git branch save/my-existing-branch my-existing-branch
# Make sure you have the latest `main` from Wagtail
git remote add upstream git@github.com:wagtail/wagtail.git
git checkout main
git pull upstream main
# Rebase onto the commit preceding the reformatting to resolve conflicts that would have been present anyway.
git checkout my-existing-branch
git rebase af942a27e41b47e257b6cd46c01a13cd381fed04^1
# Install Prettier and other Wagtail development dependencies
# Make sure you use Node v16 and npm v8
node --version
npm --version
npm install
# Test-run Prettier – this should show formatting errors.
npm run lint:format
# Rebase again, this time with the reformatting as the base,
# always preserving your changes in case of conflicts, and automatically reformatting with Prettier.
git rebase --strategy-option=theirs --exec '(npm run format || true) && git add . && git commit --amend --no-edit --no-verify' af942a27e41b47e257b6cd46c01a13cd381fed04
# Test-run Prettier – this should show no formatting errors.
npm run lint:format
# Finally rebase onto the latest version from Wagtail main, as per usual.
git rebase main
That’s it! There’s a small chance (see below) there may be conflicts with our linters, requiring further manual intervention. Run a final npm run lint
if you want to be sure.
There are a few things worth noting:
It’s a bit slow
This will reformat the whole project for every commit to be rebased, so it’s slow for PRs with lots of commits. It’d have been possible for us to only reformat the files changed by a given commit, but I was having trouble doing this in a cross-shell-compatible way.
Cases where manual changes will be needed
Once this is applied, there is still a risk there’ll be code that clashes with Prettier reformatting, which will need manual intervention. See for example 37026d521f818bab3724c74df93e9e690e93a9d5. I would expect this to be rare. As part of our main refactoring, there were two kinds of issues:
- 1 conflict with Stylelint, where Prettier splitting a long expression into multiple lines changes how the expression is interpreted by our linting.
- 8 conflicts with ESLint, where Prettier moved
eslint-disable
comments in places where they no longer applied to the correct line.
If in doubt, run npm run lint
to check for any issues of this nature.
git blame
For people who use git blame
, we have introduced a .git-blame-ignore-revs
which allows you to ignore the reformatting. See the official documentation and the file in question for further information.