prettier-plugin-hug-call-arguments
v0.1.12
Published
Prettier plugin: hug the last call argument even when it's wrapped in another call, e.g. app.delete("/x", catchAsync(async (req, res) => { ... })). Fixes https://github.com/prettier/prettier/issues/11080
Maintainers
Readme
prettier-plugin-hug-call-arguments
Fixes prettier/prettier#11080.
Usage
Requires Prettier 3.x as a peer dependency.
npm install --save-dev prettier-plugin-hug-call-argumentsyarn add --dev prettier-plugin-hug-call-argumentspnpm add --save-dev prettier-plugin-hug-call-argumentsAdd it to your Prettier config (.prettierrc, .prettierrc.json,
prettier.config.js, or the "prettier" key in package.json):
{
"plugins": ["prettier-plugin-hug-call-arguments"]
}No other options — it works automatically wherever Prettier already formats
JS/JSX/TS/TSX with the estree printer (the babel, babel-ts, and
typescript parsers). Nothing to configure, and every case it doesn't
recognize is left to Prettier's own formatting untouched.
Examples
Prettier hugs the last argument of a call when it's a function/object/array literal:
app.get("/x", (req, res) => {
res.send("ok");
});...but not when that literal is wrapped in another call, which is a very
common pattern for middleware wrappers (Express catchAsync, error
boundaries, decorators-as-functions, etc.):
// input
app.delete('/campgrounds/:id', catchAsync(async (req, res) => {
const { id } = req.params;
await Campground.findByIdAndDelete(id);
res.redirect('/campgrounds');
}));
// vanilla Prettier output
app.delete(
"/campgrounds/:id",
catchAsync(async (req, res) => {
const { id } = req.params;
await Campground.findByIdAndDelete(id);
res.redirect("/campgrounds");
}),
);
// with this plugin
app.delete("/campgrounds/:id", catchAsync(async (req, res) => {
const { id } = req.params;
await Campground.findByIdAndDelete(id);
res.redirect("/campgrounds");
}));It also flattens method chains instead of breaking every .method() onto its
own line — a common look with query builders:
// input
db.updateTable('User').set({
isDeleted: true,
profilePicture: null,
username: null,
}).where('id', '=', user.id).execute();
// vanilla Prettier output
db
.updateTable("User")
.set({
isDeleted: true,
profilePicture: null,
username: null,
})
.where("id", "=", user.id)
.execute();
// with this plugin
db.updateTable("User").set({
isDeleted: true,
profilePicture: null,
username: null,
}).where("id", "=", user.id).execute();How it works
This plugin wraps Prettier's built-in estree printer and only overrides the
print step for CallExpression, ArrayExpression, and
ArrowFunctionExpression nodes that match one of a few narrow, specific
shapes.
Preserving a layout you already chose:
// input
foo(
1,
2,
);
// with this plugin — kept exactly as written
foo(
1,
2,
);
// bar(1, 2, 3) stays on one line — nothing to preserve, so it's printed
// the normal, printWidth-aware wayPrettier already does this for object literals by default (objectWrap:
"preserve"): write { with a newline before the first property and it
stays multi-line, even if it would fit on one line. This plugin extends that
same "respect how the author wrote it" behavior to call arguments, array
elements, and arrow function parameter lists, which Prettier doesn't
normally preserve — those always auto-collapse to fit printWidth
regardless of the original formatting.
- applies once there are at least two items with a newline right after the
opening
(/[and before the first one — a single item has no "one per line" layout to preserve, so that's left to this plugin's other hugging behavior, or Prettier's own - takes priority over every other feature below: a layout you explicitly chose is never fought by this plugin's own hugging heuristics
- bails on comments anywhere in the list, a sparse array, TS type
arguments/parameters, or (for parameters) anything but a plain arrow
function — a
functionexpression/declaration has a name and block body to reproduce too, which is more than this narrow check takes on
Hugging a call-wrapped callback:
- the callee is a plain identifier or non-computed member chain (
foo,a.b.c) — chained calls (a().b()) are left untouched - either the last argument is itself a call whose own last argument is a
function/object/array literal (looked through recursively, so a chain of
wrappers like
a(b(cb))is still found — the commoncatchAsync(cb)case), or, for a 2-argument call whose second argument is short and simple (a literal or identifier), the first argument is such a wrapping call (the lodash-styleuniqueBy(collection, "key")shape) - the resulting line actually fits
printWidth; if it doesn't (most often because a wrapping call itself sits inside another wrapping call), this falls back to Prettier's default printing for the outer call — inner calls still get their own independent chance to hug once Prettier places them at their own, shallower indentation - there's no blank line between arguments, no comments on the relevant nodes, and no TS type arguments
Hugging a call whose sole argument needs it:
// input
firstValueFrom(client.getX({ fileKey: data.fileKey, frameCount: FRAME_COUNT }));
// with this plugin
firstValueFrom(client.getX({
fileKey: data.fileKey,
frameCount: FRAME_COUNT,
}));- the call has exactly one argument, and it's directly an object or array literal that needs multi-line printing
- Prettier already hugs this shape reliably in isolation, but its own "does the opening line fit" check can still give up and break the call's own parens too once this plugin has fused several outer layers together (pushing the real column deeper than Prettier expected) — there's essentially never a readability win to that fallback over just hugging directly, so this always does instead
Hugging an arrow function's concise body:
// input
const f = (data) =>
pgBoss.then((boss) => {
return boss.send(jobName, data);
});
// with this plugin
const f = (data) => pgBoss.then((boss) => {
return boss.send(jobName, data);
});- the arrow has a concise (non-block) body, no type parameters, return type,
or predicate, and default
arrowParens - the body is a
CallExpressionthat needs multi-line printing (Prettier already hugs directly after=>for other body types — objects, arrays, JSX, template literals — so there's no gap to fix there) - the resulting line fits
printWidth; otherwise this falls back to Prettier's default (a hardline after=>, body indented on its own line). The arrow's own parameter list can still break onto its own line first the normal way — only the=> bodyboundary is affected
Flattening a method chain:
- every link is a plain, non-optional
.name(...)or[expr](...)call down to a simple base (an identifier,this, or a non-computed member chain) - each link's own arguments are printed exactly as Prettier would print them standalone — a single object/array/function argument hugs the parens, a multi-argument list breaks onto its own lines only if it doesn't fit — so any number of links can break independently, correctly, with no special casing needed
- the whole flattened candidate is still rendered and measured against
printWidthas a final check (catching the one thing per-link breaking can't: many short, individually-fitting links whose combined length still overflows); if any line would overflow, it falls back to Prettier's normal chain layout
Every other node, and every CallExpression that doesn't match one of these
shapes, is delegated unchanged to Prettier's original printer.
Development
npm install
npm run build
npm test