@routecraft/prettier-plugin-routecraft 0.6.0-canary.20

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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @routecraft/prettier-plugin-routecraft
2
+
3
+ Prettier plugin that keeps Routecraft DSL chains compact and readable.
4
+
5
+ Prettier's defaults push fluent sub-route closures onto their own line and add
6
+ an extra level of indentation for every nested `.choice()`, `.when()`, and
7
+ `.otherwise()`. This plugin overrides Prettier's printer for those closures so
8
+ the threaded parameter stays on the arrow line and the chain keeps a single
9
+ indent level.
10
+
11
+ ## Before and after
12
+
13
+ ```ts
14
+ // Prettier default
15
+ .choice((c) =>
16
+ c
17
+ .when(senderInAllowlist(env.MAIL_ALLOWED_INBOUND), (b) =>
18
+ b
19
+ .enrich(agent("zoe"), only((r) => r, "agent"))
20
+ .to(mail({ action: "move" })),
21
+ )
22
+ .otherwise((b) => b),
23
+ )
24
+
25
+ // With @routecraft/prettier-plugin-routecraft
26
+ .choice((c) => c
27
+ .when(senderInAllowlist(env.MAIL_ALLOWED_INBOUND), (b) => b
28
+ .enrich(agent("zoe"), only((r) => r, "agent"))
29
+ .to(mail({ action: "move" })))
30
+ .otherwise((b) => b))
31
+ ```
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ # Bun (recommended)
37
+ bun add -D @routecraft/prettier-plugin-routecraft prettier
38
+
39
+ # npm / pnpm / yarn
40
+ npm install --save-dev @routecraft/prettier-plugin-routecraft prettier
41
+ pnpm add -D @routecraft/prettier-plugin-routecraft prettier
42
+ yarn add -D @routecraft/prettier-plugin-routecraft prettier
43
+ ```
44
+
45
+ ## Requirements
46
+
47
+ - Prettier >= 3
48
+
49
+ ## Usage
50
+
51
+ Add the plugin to your Prettier configuration:
52
+
53
+ ```js
54
+ // prettier.config.mjs
55
+ export default {
56
+ plugins: ["@routecraft/prettier-plugin-routecraft"],
57
+ };
58
+ ```
59
+
60
+ Or in `.prettierrc`:
61
+
62
+ ```json
63
+ {
64
+ "plugins": ["@routecraft/prettier-plugin-routecraft"]
65
+ }
66
+ ```
67
+
68
+ Then format as usual:
69
+
70
+ ```bash
71
+ bunx prettier --write .
72
+ ```
73
+
74
+ ## What it touches
75
+
76
+ The plugin only changes parameter-threaded builders: single-parameter arrow
77
+ closures whose body is a fluent chain rooted in that parameter, passed directly
78
+ as a call argument inside a `craft()` chain. Arrows such as
79
+ `(c) => c.when(...).otherwise(...)` and `(b) => b.enrich(...).to(...)` keep the
80
+ parameter on the arrow line instead of breaking the body onto its own line.
81
+
82
+ Everything else is left to Prettier's defaults, including factory-rooted
83
+ callbacks such as `(ex) => direct(...).send(...)` (whose chain is rooted in a
84
+ call, not the parameter), ordinary `arr.map((x) => x.foo())` chains (no
85
+ `craft()` root), arrows used as object or array values such as adapter option
86
+ callbacks (`path: (ex) => path.join(...)`), non-chain bodies such as template
87
+ literals or object literals, and async arrows or arrows with explicit return
88
+ types or type parameters (so no type information is ever dropped).
89
+
90
+ ## Documentation
91
+
92
+ For more information about Routecraft, visit [routecraft.dev](https://routecraft.dev).
93
+
94
+ ## License
95
+
96
+ Apache-2.0
97
+
98
+ ## Links
99
+
100
+ - [Documentation](https://routecraft.dev)
101
+ - [GitHub Repository](https://github.com/routecraftjs/routecraft)
102
+ - [Issue Tracker](https://github.com/routecraftjs/routecraft/issues)
@@ -0,0 +1,7 @@
1
+ import { Plugin, Parser, Printer } from 'prettier';
2
+
3
+ declare const parsers: Record<string, Parser>;
4
+ declare const printers: Record<string, Printer>;
5
+ declare const plugin: Plugin;
6
+
7
+ export { plugin as default, parsers, printers };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import {printers}from'prettier/plugins/estree';import {parsers as parsers$1}from'prettier/plugins/typescript';import {parsers}from'prettier/plugins/babel';var u="routecraft-estree";function i(e){return Array.isArray(e.comments)&&e.comments.length>0}function P(e){let t=e;for(;;){let r;switch(t.type){case "CallExpression":case "OptionalCallExpression":r=t.callee;break;case "MemberExpression":case "OptionalMemberExpression":r=t.object;break;case "ChainExpression":case "TSNonNullExpression":r=t.expression;break;default:return t}if(!r)return t;t=r;}}function p(e){let t=P(e);return t.type==="Identifier"?t.name??null:null}function D(e){return p(e)==="craft"}function N(e,t){let r=t.getParentNode(0);return !r||r.type!=="CallExpression"?false:Array.isArray(r.arguments)&&r.arguments.includes(e)}function g(e){let t=0,r=e.getParentNode(t);for(;r;){if(r.type==="CallExpression"&&D(r))return true;t+=1,r=e.getParentNode(t);}return false}function A(e,t){if(e.type!=="ArrowFunctionExpression"||e.async||e.returnType||e.typeParameters)return false;let r=e.params;if(!r||r.length!==1)return false;let s=r[0];if(!s||s.type!=="Identifier"||!s.name||s.typeAnnotation)return false;let n=e.body;return !n||n.type==="BlockStatement"||p(n)!==s.name||i(e)||i(n)?false:N(e,t)&&g(t)}var c=printers.estree,x={...c,print(e,t,r,s){let n=e.node;if(n&&A(n,e)){let f=n.params?.[0],o=(t.arrowParens??"always")==="avoid"&&!f?.typeAnnotation,m=e.call(r,"params",0),d=e.call(r,"body");return [o?"":"(",m,o?"":")"," => ",d]}return c.print(e,t,r,s)}};function a(e){return {...e,astFormat:u}}var w={typescript:a(parsers$1.typescript),babel:a(parsers.babel),"babel-ts":a(parsers["babel-ts"])},h={[u]:x},C={parsers:w,printers:h},M=C;export{M as default,w as parsers,h as printers};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["AST_FORMAT","hasComments","node","chainHead","cur","next","chainHeadName","head","rootsAtCraft","isDirectCallArgument","path","parent","isInsideCraftChain","depth","isDslArrow","params","param","body","estreePrinter","estreePrinters","routecraftPrinter","options","print","args","omitParens","paramDoc","bodyDoc","withRoutecraftPrinter","parser","parsers","typescriptParsers","babelParsers","printers","plugin","index_default"],"mappings":"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","file":"index.js","sourcesContent":["import type { AstPath, Doc, Parser, Plugin, Printer } from \"prettier\";\nimport { printers as estreePrinters } from \"prettier/plugins/estree\";\nimport { parsers as typescriptParsers } from \"prettier/plugins/typescript\";\nimport { parsers as babelParsers } from \"prettier/plugins/babel\";\n\n/**\n * Name of the AST format this plugin registers. The wrapped parsers point\n * their `astFormat` here so Prettier dispatches every node to our printer,\n * which delegates back to the built-in estree printer for everything that is\n * not a Routecraft DSL closure.\n */\nconst AST_FORMAT = \"routecraft-estree\";\n\n/**\n * Minimal structural view of the estree-like nodes this plugin inspects.\n * Prettier does not export estree node types, so we model only the fields we\n * read while walking fluent chains. Optional fields cover the variations the\n * TypeScript and Babel parsers can emit (optional chaining, non-null\n * assertions, type annotations).\n */\ninterface DslNode {\n type: string;\n name?: string;\n async?: boolean;\n object?: DslNode;\n callee?: DslNode;\n expression?: DslNode;\n body?: DslNode;\n params?: DslNode[];\n arguments?: DslNode[];\n comments?: unknown[];\n typeAnnotation?: unknown;\n returnType?: unknown;\n typeParameters?: unknown;\n}\n\n/** Whether Prettier attached any comments directly to this node. */\nfunction hasComments(node: DslNode): boolean {\n return Array.isArray(node.comments) && node.comments.length > 0;\n}\n\n/**\n * Walk to the head of a fluent member/call chain and return the terminal node\n * (the first node that is not a chain link). For `c.when(...).otherwise(...)`\n * this is the identifier `c`; for `direct(...).send(...)` it is the `direct`\n * identifier reached through the leading call. Callers inspect the result to\n * distinguish parameter-threaded builders from factory-rooted callbacks and to\n * detect `craft()`-rooted chains.\n */\nfunction chainHead(node: DslNode): DslNode {\n let cur: DslNode = node;\n while (true) {\n let next: DslNode | undefined;\n switch (cur.type) {\n case \"CallExpression\":\n case \"OptionalCallExpression\":\n next = cur.callee;\n break;\n case \"MemberExpression\":\n case \"OptionalMemberExpression\":\n next = cur.object;\n break;\n case \"ChainExpression\":\n case \"TSNonNullExpression\":\n next = cur.expression;\n break;\n default:\n return cur;\n }\n if (!next) return cur;\n cur = next;\n }\n}\n\n/**\n * The identifier name at the head of a chain, or `null` when it does not bottom\n * out at a bare identifier. Used to tell parameter-threaded builders\n * (`(c) => c.when(...)`, head is the param) from factory-rooted callbacks\n * (`(ex) => direct(...).send(...)`, head is a call).\n */\nfunction chainHeadName(node: DslNode): string | null {\n const head = chainHead(node);\n return head.type === \"Identifier\" ? (head.name ?? null) : null;\n}\n\n/**\n * Whether a chain ultimately roots at a `craft()` call. This is what scopes\n * the plugin to the Routecraft DSL: arbitrary fluent chains such as\n * `arr.map(...)` are left to Prettier's defaults.\n */\nfunction rootsAtCraft(node: DslNode): boolean {\n return chainHeadName(node) === \"craft\";\n}\n\n/**\n * Whether the node sits directly in a call's argument list, for example the\n * closure in `.enrich((ex) => ..., only(...))`. This keeps the plugin to DSL\n * callback arguments and leaves arrows that are object or array values (such as\n * `path: (ex) => path.join(...)` inside an adapter's options) to Prettier.\n */\nfunction isDirectCallArgument(node: DslNode, path: AstPath): boolean {\n const parent = path.getParentNode(0) as DslNode | null;\n if (!parent || parent.type !== \"CallExpression\") return false;\n return Array.isArray(parent.arguments) && parent.arguments.includes(node);\n}\n\n/** Whether any ancestor call expression is part of a `craft()` chain. */\nfunction isInsideCraftChain(path: AstPath): boolean {\n let depth = 0;\n let parent = path.getParentNode(depth) as DslNode | null;\n while (parent) {\n if (parent.type === \"CallExpression\" && rootsAtCraft(parent)) {\n return true;\n }\n depth += 1;\n parent = path.getParentNode(depth) as DslNode | null;\n }\n return false;\n}\n\n/**\n * A \"DSL arrow\" is a single-parameter arrow that threads its parameter straight\n * into a fluent chain (`(c) => c.when(...).otherwise(...)`, or the trivial\n * `(b) => b`), passed directly as a call argument inside a `craft()` chain.\n * These are the sub-route builder closures Prettier breaks across too many\n * lines, so we keep the parameter on the arrow line.\n *\n * Everything else falls through to Prettier: factory-rooted callbacks such as\n * `(ex) => direct(...).send(...)` (whose head is a call, not the parameter) get\n * Prettier's natural arrow layout, which keeps short bodies inline and breaks\n * long ones onto the next line. Async arrows and arrows with explicit return\n * types or type parameters are left untouched so we never drop a type\n * annotation, as are arrows used as object or array values (adapter option\n * callbacks) and non-chain bodies (template literals, conditionals, ...).\n */\nfunction isDslArrow(node: DslNode, path: AstPath): boolean {\n if (node.type !== \"ArrowFunctionExpression\") return false;\n if (node.async) return false;\n if (node.returnType || node.typeParameters) return false;\n\n const params = node.params;\n if (!params || params.length !== 1) return false;\n\n const param = params[0];\n if (!param || param.type !== \"Identifier\" || !param.name) return false;\n\n // Bail when the parameter is typed. Forcing the arrow inline can make Prettier\n // break the type annotation across lines to fit, which is uglier than its\n // default layout (which breaks after `=>` and keeps the annotation intact).\n if (param.typeAnnotation) return false;\n\n const body = node.body;\n if (!body || body.type === \"BlockStatement\") return false;\n if (chainHeadName(body) !== param.name) return false;\n\n // Bail to Prettier's default printer when the arrow or its immediate body\n // carries comments. Our hand-built doc does not reproduce the default arrow\n // printer's comment placement, and a comment between `=>` and the body would\n // otherwise produce non-idempotent output. Comments deeper in the chain\n // attach to other nodes and are printed correctly via recursion.\n if (hasComments(node) || hasComments(body)) return false;\n\n return isDirectCallArgument(node, path) && isInsideCraftChain(path);\n}\n\nconst estreePrinter = estreePrinters.estree;\n\n/**\n * Our printer wraps the built-in estree printer. For parameter-threaded DSL\n * builders (`(c) => c.when(...)`) it keeps the parameter on the arrow line and\n * lets the body's own member-chain layout supply the indentation, collapsing\n * the extra \"body on its own line\" break and one level of indentation Prettier\n * would otherwise add. Every other node falls through to the built-in printer\n * unchanged.\n */\nconst routecraftPrinter: Printer = {\n ...estreePrinter,\n print(path, options, print, args): Doc {\n const node = path.node as DslNode;\n if (node && isDslArrow(node, path)) {\n const param = node.params?.[0];\n const arrowParens = options.arrowParens ?? \"always\";\n const omitParens = arrowParens === \"avoid\" && !param?.typeAnnotation;\n const paramDoc = path.call(print, \"params\", 0);\n const bodyDoc = path.call(print, \"body\");\n const open = omitParens ? \"\" : \"(\";\n const close = omitParens ? \"\" : \")\";\n return [open, paramDoc, close, \" => \", bodyDoc];\n }\n return estreePrinter.print(path, options, print, args);\n },\n};\n\n/** Wrap a built-in parser so it dispatches to our printer. */\nfunction withRoutecraftPrinter(parser: Parser): Parser {\n return { ...parser, astFormat: AST_FORMAT };\n}\n\nexport const parsers: Record<string, Parser> = {\n typescript: withRoutecraftPrinter(typescriptParsers.typescript),\n babel: withRoutecraftPrinter(babelParsers.babel),\n \"babel-ts\": withRoutecraftPrinter(babelParsers[\"babel-ts\"]),\n};\n\nexport const printers: Record<string, Printer> = {\n [AST_FORMAT]: routecraftPrinter,\n};\n\nconst plugin: Plugin = { parsers, printers };\n\nexport default plugin;\n"]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@routecraft/prettier-plugin-routecraft",
3
+ "version": "0.6.0-canary.20",
4
+ "description": "Prettier plugin for compact Routecraft DSL formatting",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup src/index.ts --format esm --dts",
13
+ "prepublishOnly": "bun run build",
14
+ "test": "vitest"
15
+ },
16
+ "peerDependencies": {
17
+ "prettier": ">=3"
18
+ },
19
+ "devDependencies": {
20
+ "prettier": "^3.8.3",
21
+ "tsup": "^8.5.1",
22
+ "typescript": "^5.9.3",
23
+ "vitest": "^4.1.5"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": "https://github.com/routecraftjs/routecraft",
29
+ "keywords": [
30
+ "prettier",
31
+ "prettier-plugin",
32
+ "routecraft",
33
+ "formatting",
34
+ "typescript"
35
+ ],
36
+ "author": "routecraftjs",
37
+ "license": "Apache-2.0",
38
+ "homepage": "https://routecraft.dev"
39
+ }