prettier-plugin-hug-call-arguments 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 soso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # prettier-plugin-hug-call-arguments
2
+
3
+ Fixes [prettier/prettier#11080](https://github.com/prettier/prettier/issues/11080).
4
+
5
+ Prettier hugs the last argument of a call when it's a function/object/array
6
+ literal:
7
+
8
+ ```js
9
+ app.get("/x", (req, res) => {
10
+ res.send("ok");
11
+ });
12
+ ```
13
+
14
+ ...but not when that literal is wrapped in another call, which is a very
15
+ common pattern for middleware wrappers (Express `catchAsync`, error
16
+ boundaries, decorators-as-functions, etc.):
17
+
18
+ ```js
19
+ // input
20
+ app.delete('/campgrounds/:id', catchAsync(async (req, res) => {
21
+ const { id } = req.params;
22
+ await Campground.findByIdAndDelete(id);
23
+ res.redirect('/campgrounds');
24
+ }));
25
+
26
+ // vanilla Prettier output
27
+ app.delete(
28
+ "/campgrounds/:id",
29
+ catchAsync(async (req, res) => {
30
+ const { id } = req.params;
31
+ await Campground.findByIdAndDelete(id);
32
+ res.redirect("/campgrounds");
33
+ }),
34
+ );
35
+
36
+ // with this plugin
37
+ app.delete("/campgrounds/:id", catchAsync(async (req, res) => {
38
+ const { id } = req.params;
39
+ await Campground.findByIdAndDelete(id);
40
+ res.redirect("/campgrounds");
41
+ }));
42
+ ```
43
+
44
+ ## How it works
45
+
46
+ This plugin wraps Prettier's built-in `estree` printer and only overrides the
47
+ `print` step for `CallExpression` nodes that match a narrow, specific shape:
48
+
49
+ - the callee is a plain identifier or non-computed member chain (`foo`,
50
+ `a.b.c`) — chained calls (`a().b()`) are left untouched
51
+ - the last argument is itself a call whose own last argument is a
52
+ function/object/array literal (looked through recursively)
53
+ - every earlier argument fits on the opening line
54
+ - there's no blank line between arguments, no comments on the relevant
55
+ nodes, and no TS type arguments
56
+
57
+ Every other node, and every `CallExpression` that doesn't match that shape,
58
+ is delegated unchanged to Prettier's original printer.
59
+
60
+ ## Usage
61
+
62
+ ```sh
63
+ npm install --save-dev prettier-plugin-hug-call-arguments
64
+ ```
65
+
66
+ `.prettierrc`:
67
+
68
+ ```json
69
+ {
70
+ "plugins": ["prettier-plugin-hug-call-arguments"]
71
+ }
72
+ ```
73
+
74
+ ## Development
75
+
76
+ ```sh
77
+ npm install
78
+ npm run build
79
+ npm test
80
+ ```
@@ -0,0 +1,18 @@
1
+ import type { Plugin } from "prettier";
2
+ /** Minimal structural view of the ESTree/Babel nodes this plugin touches. */
3
+ interface ESNode {
4
+ type: string;
5
+ start?: number;
6
+ end?: number;
7
+ comments?: unknown[];
8
+ optional?: boolean;
9
+ computed?: boolean;
10
+ typeArguments?: unknown;
11
+ typeParameters?: unknown;
12
+ callee?: ESNode;
13
+ object?: ESNode;
14
+ arguments?: ESNode[];
15
+ body?: ESNode;
16
+ }
17
+ declare const plugin: Plugin<ESNode>;
18
+ export = plugin;
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ // Fixes https://github.com/prettier/prettier/issues/11080
3
+ //
4
+ // Prettier "hugs" the last argument of a call and keeps the earlier
5
+ // arguments on the opening line when the last argument is itself a
6
+ // function/object/array literal, e.g.:
7
+ //
8
+ // app.get("/x", (req, res) => {
9
+ // ...
10
+ // });
11
+ //
12
+ // But it refuses to do that when the last argument is a *call* that wraps
13
+ // such a literal, e.g. Express-style middleware wrappers:
14
+ //
15
+ // app.delete("/campgrounds/:id", catchAsync(async (req, res) => {
16
+ // ...
17
+ // }));
18
+ //
19
+ // In that case Prettier falls back to breaking every argument onto its own
20
+ // line. This plugin extends the "can this be hugged" check to look through
21
+ // one or more wrapping calls, and reproduces Prettier's own hugging layout
22
+ // for that case. Every other case is delegated to the original estree
23
+ // printer, untouched.
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ const doc_1 = require("prettier/doc");
58
+ // `prettier/plugins/estree` ships CommonJS + ESM builds; the CJS build is
59
+ // what `require`/interop resolves to, and its printer object is what we
60
+ // spread and delegate to for every node we don't special-case.
61
+ const estree = __importStar(require("prettier/plugins/estree"));
62
+ const { group, conditionalGroup, indent, hardline, join, breakParent } = doc_1.builders;
63
+ const { willBreak } = doc_1.utils;
64
+ // `estree.printers.estree` is typed as `Printer<any>` by Prettier, which is
65
+ // assignable to `Printer<ESNode>` on its own — no cast needed.
66
+ const estreePrinter = estree.printers.estree;
67
+ function hasComment(node) {
68
+ return Boolean(node?.comments && node.comments.length > 0);
69
+ }
70
+ function isHuggableFunction(node) {
71
+ if (!node) {
72
+ return false;
73
+ }
74
+ if (node.type === "FunctionExpression") {
75
+ return true;
76
+ }
77
+ if (node.type === "ArrowFunctionExpression") {
78
+ const body = node.body;
79
+ return (body?.type === "BlockStatement" ||
80
+ body?.type === "ObjectExpression" ||
81
+ body?.type === "ArrayExpression");
82
+ }
83
+ return false;
84
+ }
85
+ /**
86
+ * Like Prettier's internal `couldExpandArg`, but also looks through calls
87
+ * that merely wrap a huggable function, e.g. `catchAsync(async () => {})`.
88
+ */
89
+ function couldExpandArg(node, depth = 0) {
90
+ if (!node || depth > 4) {
91
+ return false;
92
+ }
93
+ if (isHuggableFunction(node)) {
94
+ return true;
95
+ }
96
+ if (node.type === "CallExpression" &&
97
+ !node.optional &&
98
+ !hasComment(node) &&
99
+ node.arguments &&
100
+ node.arguments.length > 0 &&
101
+ !node.typeArguments &&
102
+ !node.typeParameters) {
103
+ return couldExpandArg(node.arguments.at(-1), depth + 1);
104
+ }
105
+ return false;
106
+ }
107
+ /**
108
+ * Only intervene for plain-looking callees (`foo(...)`, `a.b.c(...)`) so we
109
+ * never fight Prettier's member/call-chain printing (`a().b().c(cb)`).
110
+ */
111
+ function isSimpleCallee(callee) {
112
+ if (callee.type === "Identifier" ||
113
+ callee.type === "ThisExpression" ||
114
+ callee.type === "Super") {
115
+ return true;
116
+ }
117
+ if (callee.type === "MemberExpression" && !callee.computed && callee.object) {
118
+ return isSimpleCallee(callee.object);
119
+ }
120
+ return false;
121
+ }
122
+ function hasBlankLineBetween(originalText, start, end) {
123
+ return /\n[^\S\n]*\n/.test(originalText.slice(start, end));
124
+ }
125
+ function tryHugWrappedCallback(path, options, print) {
126
+ const { node } = path;
127
+ if (node.type !== "CallExpression" || node.optional) {
128
+ return undefined;
129
+ }
130
+ if (node.typeArguments || node.typeParameters) {
131
+ return undefined;
132
+ }
133
+ const args = node.arguments ?? [];
134
+ if (args.length === 0 || !node.callee || !isSimpleCallee(node.callee)) {
135
+ return undefined;
136
+ }
137
+ const lastArg = args.at(-1);
138
+ if (!lastArg ||
139
+ hasComment(lastArg) ||
140
+ lastArg.type !== "CallExpression" ||
141
+ lastArg.optional ||
142
+ !lastArg.arguments ||
143
+ lastArg.arguments.length === 0 ||
144
+ !couldExpandArg(lastArg)) {
145
+ return undefined;
146
+ }
147
+ for (let i = 0; i < args.length - 1; i++) {
148
+ const current = args[i];
149
+ const next = args[i + 1];
150
+ if (current?.end !== undefined &&
151
+ next?.start !== undefined &&
152
+ hasBlankLineBetween(options.originalText, current.end, next.start)) {
153
+ return undefined;
154
+ }
155
+ }
156
+ const printedArgs = path.map(print, "arguments");
157
+ const headDocs = printedArgs.slice(0, -1);
158
+ const lastDoc = printedArgs.at(-1);
159
+ // If the earlier args don't fit on one line, or the wrapped callback
160
+ // wouldn't break anyway, Prettier's default output is already fine.
161
+ if (lastDoc === undefined ||
162
+ headDocs.some((doc) => willBreak(doc)) ||
163
+ !willBreak(lastDoc)) {
164
+ return undefined;
165
+ }
166
+ const headWithCommas = headDocs.flatMap((doc) => [doc, ", "]);
167
+ const trailingComma = options.trailingComma === "all" ? "," : "";
168
+ const allArgsBrokenOut = () => group([
169
+ "(",
170
+ indent([hardline, join([",", hardline], printedArgs)]),
171
+ trailingComma,
172
+ hardline,
173
+ ")",
174
+ ], { shouldBreak: true });
175
+ return [
176
+ breakParent,
177
+ print("callee"),
178
+ conditionalGroup([
179
+ ["(", ...headWithCommas, group(lastDoc, { shouldBreak: true }), ")"],
180
+ allArgsBrokenOut(),
181
+ ]),
182
+ ];
183
+ }
184
+ const plugin = {
185
+ printers: {
186
+ estree: {
187
+ ...estreePrinter,
188
+ print(path, options, print, args) {
189
+ const hugged = tryHugWrappedCallback(path, options, print);
190
+ return hugged === undefined
191
+ ? estreePrinter.print(path, options, print, args)
192
+ : hugged;
193
+ },
194
+ },
195
+ },
196
+ };
197
+ module.exports = plugin;
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "prettier-plugin-hug-call-arguments",
3
+ "version": "0.1.0",
4
+ "description": "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",
5
+ "author": "soso tsertsvadze",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "require": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE",
20
+ "package.json"
21
+ ],
22
+ "scripts": {
23
+ "clean": "rm -rf dist dist-test",
24
+ "build": "npm run clean && tsc --project tsconfig.json",
25
+ "test": "npm run build && tsc --project tsconfig.test.json && node --test ./dist-test/*.js && rm -rf ./dist-test",
26
+ "prepublishOnly": "npm run test"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "registry": "https://registry.npmjs.org/"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Generalsimus/prettier-plugin-hug-call-arguments.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/Generalsimus/prettier-plugin-hug-call-arguments/issues"
38
+ },
39
+ "homepage": "https://github.com/Generalsimus/prettier-plugin-hug-call-arguments#readme",
40
+ "keywords": [
41
+ "prettier",
42
+ "prettier-plugin",
43
+ "formatter",
44
+ "code-style",
45
+ "call-arguments",
46
+ "callback",
47
+ "typescript"
48
+ ],
49
+ "peerDependencies": {
50
+ "prettier": ">=3.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^26.3.0",
54
+ "prettier": "^3.9.6",
55
+ "typescript": "^7.0.2"
56
+ }
57
+ }