@contractkit/prettier-plugin 0.12.2 → 0.13.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.
@@ -5,9 +5,9 @@ $ tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly
5
5
  CLI tsup v8.5.1
6
6
  CLI Target: esnext
7
7
  ESM Build start
8
- ESM dist/index.js 21.38 KB
9
- ESM dist/index.js.map 52.77 KB
10
- ESM ⚡️ Build success in 206ms
8
+ ESM dist/index.js 24.87 KB
9
+ ESM dist/index.js.map 62.23 KB
10
+ ESM ⚡️ Build success in 191ms
11
11
  DTS Build start
12
- DTS ⚡️ Build success in 5108ms
13
- DTS dist/index.d.ts 726.00 B
12
+ DTS ⚡️ Build success in 4964ms
13
+ DTS dist/index.d.ts 1.13 KB
@@ -3,21 +3,24 @@ $ vitest run --coverage
3
3
   RUN  v4.1.5 /home/runner/work/ContractKit/ContractKit/apps/prettier-plugin
4
4
  Coverage enabled with v8
5
5
 
6
- ✓ tests/print-ck.test.ts (75 tests) 570ms
6
+ ✓ tests/print-ck.test.ts (81 tests) 676ms
7
+ ✓ tests/round-trip.test.ts (20 tests) 1570ms
8
+ ✓ formats test.ck to itself  587ms
9
+ ✓ formatting is a fixed point for source #0  769ms
7
10
 
8
-  Test Files  1 passed (1)
9
-  Tests  75 passed (75)
10
-  Start at  17:06:39
11
-  Duration  6.62s (transform 2.00s, setup 0ms, import 4.69s, tests 570ms, environment 0ms)
11
+  Test Files  2 passed (2)
12
+  Tests  101 passed (101)
13
+  Start at  15:07:02
14
+  Duration  7.40s (transform 3.64s, setup 0ms, import 9.29s, tests 2.25s, environment 0ms)
12
15
 
13
16
   % Coverage report from v8
14
17
  -------------------|---------|----------|---------|---------|-------------------
15
18
  File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
16
19
  -------------------|---------|----------|---------|---------|-------------------
17
- All files | 71.2 | 63.72 | 81.25 | 77.8 |
20
+ All files | 75.62 | 69.04 | 84.21 | 81.34 |
18
21
  indent.ts | 100 | 100 | 100 | 100 |
19
- print-ck.ts | 89.18 | 83.8 | 100 | 98.14 | 70
20
- print-contract.ts | 45.71 | 37.5 | 50 | 46.66 | 17,47-78
21
- ...t-operation.ts | 73.98 | 65.04 | 92.85 | 83.95 | ...88,296-307,339
22
- print-type.ts | 58.69 | 56.73 | 63.63 | 60 | ...02-103,148-151
22
+ print-ck.ts | 88.34 | 85 | 100 | 97.18 | 80-81
23
+ print-contract.ts | 45.71 | 38.23 | 50 | 46.66 | 20,52-85
24
+ ...t-operation.ts | 78.32 | 69.46 | 94.44 | 86.12 | ...37,354,362-373
25
+ print-type.ts | 64.13 | 63.46 | 63.63 | 66.66 | ...02-103,148-151
23
26
  -------------------|---------|----------|---------|---------|-------------------
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @contractkit/prettier-plugin-contractkit
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 23e4beb: Fix the formatter rewriting `.ck` files it should have left alone. Running Prettier on a contract folded standalone `#` comment blocks into a trailing comment on the following declaration (`# ─── Pet endpoints ───` became `operation /pet: { # ─── Pet endpoints ───`), reordered operation body keys into a canonical order, dropped blank lines between operations, and expanded single-line response bodies like `200: { application/json: Pet }` onto three lines. An inline contract comment (`contract Pet: { # A pet`) was also attributed to the first field, so it printed twice.
8
+
9
+ The parser now records the author's layout alongside the semantics — comment placement (`leadingComments`, `descriptionInline`), operation body key order (`keyOrder`), blank lines (`blankLineBefore`), and single-line response blocks (`inline`) — and the printer reproduces it. A `#` comment separated from the declaration below it by a blank line is a standalone divider rather than a doc comment; one written directly above is a doc comment and is emitted above the declaration, not on its header line.
10
+
11
+ Comments may now also sit directly inside an `options { ... }` block, between its sub-blocks, where the grammar previously rejected them.
12
+
13
+ These AST fields are additive and optional; codegen plugins ignore them.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [23e4beb]
18
+ - @contractkit/core@0.24.0
19
+
3
20
  ## 0.12.2
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -33,12 +33,10 @@ Most editors with a Prettier integration (VS Code, JetBrains, Neovim) pick the p
33
33
 
34
34
  ## What it does
35
35
 
36
- The printer round-trips the parser's AST back into canonical `.ck` source:
36
+ The printer round-trips the parser's AST back into `.ck` source:
37
37
 
38
38
  - 4-space indentation (matches Prettier's default `tabWidth`)
39
39
  - Canonical modifier order on fields: `override → deprecated → readonly|writeonly`
40
- - Stable ordering of `options` block items, route bodies, and operation blocks
41
- - Inline `# comment` placement preserved on field/operation/status lines
42
40
  - Multi-base inheritance: `contract C: A & B & { ... }` with the inline block always last
43
41
  - Multi-line unions: a leading `|` is preserved on type aliases like `contract X: A | B | C`
44
42
  - Discriminated unions render as `discriminated(by=field, A | B | C)`
@@ -46,6 +44,17 @@ The printer round-trips the parser's AST back into canonical `.ck` source:
46
44
 
47
45
  The plugin honours Prettier's `printWidth` for line-wrapping decisions where applicable, but most CK constructs format to a fixed multi-line shape regardless of width.
48
46
 
47
+ ## What it preserves
48
+
49
+ Formatting a well-formed `.ck` file leaves it byte-identical. The formatter deliberately does **not** impose a canonical layout where the language allows more than one form — it reproduces what the author wrote:
50
+
51
+ - **Comment placement.** A `#` block separated from the declaration below it by a blank line stays a standalone divider; one directly above becomes that declaration's doc comment and is re-emitted above it, not folded onto the header line. A comment written inline (`contract Pet: { # ...`) stays inline.
52
+ - **Operation body key order.** `sdk` before `service` stays that way; the printer never sorts a user's keys into a canonical order.
53
+ - **Blank lines** between operations inside a route.
54
+ - **Single-line response blocks.** `200: { application/json: Pet }` is not expanded, and an expanded block is not collapsed.
55
+
56
+ This is covered by `tests/round-trip.test.ts`, which formats every `.ck` file under `contracts/` and asserts the output is unchanged, plus checks that formatting is a fixed point. Anything that makes the printer normalize rather than preserve will fail it.
57
+
49
58
  ## Source layout
50
59
 
51
60
  | Path | Purpose |
package/dist/index.js CHANGED
@@ -150,7 +150,7 @@ function printModelDecl(model, printWidth = 80) {
150
150
  if (model.type !== void 0) {
151
151
  return printTypeAlias(model, printWidth);
152
152
  }
153
- const commentSuffix = model.description ? ` # ${model.description}` : "";
153
+ const commentSuffix = model.description && model.descriptionInline ? ` # ${model.description}` : "";
154
154
  const modifiers = [
155
155
  model.deprecated ? "deprecated" : "",
156
156
  model.inputCase || model.outputCase ? `format(${[
@@ -177,7 +177,7 @@ function printModelDecl(model, printWidth = 80) {
177
177
  __name(printModelDecl, "printModelDecl");
178
178
  function printTypeAlias(model, printWidth) {
179
179
  const type = model.type;
180
- const commentSuffix = model.description ? ` # ${model.description}` : "";
180
+ const commentSuffix = model.description && model.descriptionInline ? ` # ${model.description}` : "";
181
181
  const modifiers = [
182
182
  model.deprecated ? "deprecated" : "",
183
183
  model.inputCase || model.outputCase ? `format(${[
@@ -222,8 +222,7 @@ function flushBlocks(out, blocks, idx, beforeLine, _indent = "") {
222
222
  __name(flushBlocks, "flushBlocks");
223
223
  function printRoute(route, blocks, idx, nextRouteStart) {
224
224
  const lines = [];
225
- const commentSuffix = route.description ? ` # ${route.description}` : "";
226
- lines.push(`${route.path}: {${commentSuffix}`);
225
+ lines.push(`${route.path}: {`);
227
226
  if (route.params !== void 0) {
228
227
  lines.push(...printParamsBlock(route.params, I1, route.paramsMode));
229
228
  }
@@ -231,6 +230,7 @@ function printRoute(route, blocks, idx, nextRouteStart) {
231
230
  lines.push(...printSecurity(route.security, I1, I2));
232
231
  }
233
232
  for (const op of route.operations) {
233
+ if (op.blankLineBefore && lines.length > 1) lines.push("");
234
234
  flushBlocks(lines, blocks, idx, op.loc.line, I1);
235
235
  lines.push(...printOperation(op));
236
236
  }
@@ -270,53 +270,131 @@ function printParamsBlock(source, indent, mode) {
270
270
  ];
271
271
  }
272
272
  __name(printParamsBlock, "printParamsBlock");
273
- function printOperation(op) {
274
- const lines = [];
275
- const commentSuffix = op.description ? ` # ${op.description}` : "";
276
- const modPart = op.modifiers?.length ? `(${op.modifiers[0]})` : "";
277
- lines.push(`${I1}${op.method}${modPart}: {${commentSuffix}`);
278
- if (op.name) lines.push(`${I2}name: ${op.name}`);
279
- if (op.service) lines.push(`${I2}service: ${op.service}`);
280
- if (op.sdk) lines.push(`${I2}sdk: ${op.sdk}`);
281
- if (op.signature) {
282
- const comment = op.signatureDescription ? ` # ${op.signatureDescription}` : "";
283
- if (op.signaturePolicy) {
284
- lines.push(`${I2}signature: {`);
285
- lines.push(`${I3}options: ${formatSignatureValue(op.signature)}${comment}`);
286
- lines.push(`${I3}policy: ${op.signaturePolicy}`);
273
+ var CANONICAL_KEY_ORDER = [
274
+ "name",
275
+ "service",
276
+ "sdk",
277
+ "mcp",
278
+ "signature",
279
+ "security",
280
+ "plugins",
281
+ "query",
282
+ "headers",
283
+ "request",
284
+ "responses"
285
+ ];
286
+ function printOperationKey(op, key) {
287
+ switch (key) {
288
+ case "name":
289
+ return op.name ? [
290
+ `${I2}name: ${op.name}`
291
+ ] : [];
292
+ case "service":
293
+ return op.service ? [
294
+ `${I2}service: ${op.service}`
295
+ ] : [];
296
+ case "sdk":
297
+ return op.sdk ? [
298
+ `${I2}sdk: ${op.sdk}`
299
+ ] : [];
300
+ case "mcp":
301
+ if (op.mcp === true) return [
302
+ `${I2}mcp: true`
303
+ ];
304
+ if (op.mcp === false) return [
305
+ `${I2}mcp: false`
306
+ ];
307
+ return op.mcp ? printMcpBlock(op.mcp) : [];
308
+ case "signature": {
309
+ if (!op.signature) return [];
310
+ const comment = op.signatureDescription ? ` # ${op.signatureDescription}` : "";
311
+ if (op.signaturePolicy) {
312
+ return [
313
+ `${I2}signature: {`,
314
+ `${I3}options: ${formatSignatureValue(op.signature)}${comment}`,
315
+ `${I3}policy: ${op.signaturePolicy}`,
316
+ `${I2}}`
317
+ ];
318
+ }
319
+ return [
320
+ `${I2}signature: ${formatSignatureValue(op.signature)}${comment}`
321
+ ];
322
+ }
323
+ case "security":
324
+ return op.security !== void 0 ? printSecurity(op.security) : [];
325
+ case "plugins": {
326
+ if (!op.plugins || Object.keys(op.plugins).length === 0) return [];
327
+ const lines = [
328
+ `${I2}plugins: {`
329
+ ];
330
+ for (const [k, val] of Object.entries(op.plugins)) lines.push(...printPluginEntry(k, val, I3));
287
331
  lines.push(`${I2}}`);
288
- } else {
289
- lines.push(`${I2}signature: ${formatSignatureValue(op.signature)}${comment}`);
332
+ return lines;
290
333
  }
291
- }
292
- if (op.security !== void 0) lines.push(...printSecurity(op.security));
293
- if (op.plugins && Object.keys(op.plugins).length > 0) {
294
- lines.push(`${I2}plugins: {`);
295
- for (const [key, val] of Object.entries(op.plugins)) {
296
- lines.push(...printPluginEntry(key, val, I3));
334
+ case "query":
335
+ return op.query !== void 0 ? printQueryOrHeaders("query", op.query, op.queryMode) : [];
336
+ case "headers":
337
+ if (op.requestHeadersOptOut) return [
338
+ `${I2}headers: none`
339
+ ];
340
+ return op.headers !== void 0 ? printQueryOrHeaders("headers", op.headers, op.headersMode) : [];
341
+ case "request": {
342
+ if (!op.request) return [];
343
+ const lines = [
344
+ `${I2}request: {`
345
+ ];
346
+ for (const body of op.request.bodies) lines.push(...printContentTypeLine(body.contentType, body.bodyType, I3));
347
+ lines.push(`${I2}}`);
348
+ return lines;
297
349
  }
298
- lines.push(`${I2}}`);
299
- }
300
- if (op.query !== void 0) lines.push(...printQueryOrHeaders("query", op.query, op.queryMode));
301
- if (op.requestHeadersOptOut) {
302
- lines.push(`${I2}headers: none`);
303
- } else if (op.headers !== void 0) {
304
- lines.push(...printQueryOrHeaders("headers", op.headers, op.headersMode));
350
+ case "responses":
351
+ return op.responses.length > 0 ? printResponseBlock(op.responses) : [];
305
352
  }
306
- if (op.request) {
307
- lines.push(`${I2}request: {`);
308
- for (const body of op.request.bodies) {
309
- lines.push(...printContentTypeLine(body.contentType, body.bodyType, I3));
310
- }
311
- lines.push(`${I2}}`);
353
+ }
354
+ __name(printOperationKey, "printOperationKey");
355
+ function printOperation(op) {
356
+ const lines = [];
357
+ const modPart = op.modifiers?.length ? `(${op.modifiers[0]})` : "";
358
+ const inlineDescription = op.descriptionInline ?? true;
359
+ if (op.description && !inlineDescription) {
360
+ for (const line of op.description.split("\n")) lines.push(`${I1}# ${line}`);
312
361
  }
313
- if (op.responses.length > 0) {
314
- lines.push(...printResponseBlock(op.responses));
362
+ const commentSuffix = op.description && inlineDescription ? ` # ${op.description}` : "";
363
+ lines.push(`${I1}${op.method}${modPart}: {${commentSuffix}`);
364
+ const order = op.keyOrder ?? [];
365
+ const rest = CANONICAL_KEY_ORDER.filter((k) => !order.includes(k));
366
+ for (const key of [
367
+ ...order,
368
+ ...rest
369
+ ]) {
370
+ lines.push(...printOperationKey(op, key));
315
371
  }
316
372
  lines.push(`${I1}}`);
317
373
  return lines;
318
374
  }
319
375
  __name(printOperation, "printOperation");
376
+ function mcpHintTokens(mcp) {
377
+ const tokens = [];
378
+ if (mcp.readOnlyHint !== void 0) tokens.push(mcp.readOnlyHint ? "readOnly" : "nonReadOnly");
379
+ if (mcp.idempotentHint !== void 0) tokens.push(mcp.idempotentHint ? "idempotent" : "nonIdempotent");
380
+ if (mcp.destructiveHint !== void 0) tokens.push(mcp.destructiveHint ? "destructive" : "nonDestructive");
381
+ if (mcp.openWorldHint !== void 0) tokens.push(mcp.openWorldHint ? "openWorld" : "closedWorld");
382
+ return tokens;
383
+ }
384
+ __name(mcpHintTokens, "mcpHintTokens");
385
+ function printMcpBlock(mcp) {
386
+ const lines = [
387
+ `${I2}mcp: {`
388
+ ];
389
+ if (mcp.name !== void 0) lines.push(`${I3}name: "${escapeString(mcp.name)}"`);
390
+ if (mcp.title !== void 0) lines.push(`${I3}title: "${escapeString(mcp.title)}"`);
391
+ if (mcp.description !== void 0) lines.push(`${I3}description: "${escapeString(mcp.description)}"`);
392
+ const tokens = mcpHintTokens(mcp);
393
+ if (tokens.length > 0) lines.push(`${I3}hint: ${tokens.join(", ")}`);
394
+ lines.push(`${I2}}`);
395
+ return lines;
396
+ }
397
+ __name(printMcpBlock, "printMcpBlock");
320
398
  var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
321
399
  function escapeString(s) {
322
400
  return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -461,7 +539,9 @@ function printResponseBlock(responses) {
461
539
  const hasBody = resp.contentType && resp.bodyType;
462
540
  const hasHeaders = resp.headers && resp.headers.length > 0;
463
541
  const optOut = resp.headersOptOut;
464
- if (hasBody || hasHeaders || optOut) {
542
+ if (resp.inline && hasBody && !hasHeaders && !optOut && resp.bodyType.kind !== "inlineObject") {
543
+ lines.push(`${I3}${resp.statusCode}: { ${resp.contentType}: ${printType(resp.bodyType)} }`);
544
+ } else if (hasBody || hasHeaders || optOut) {
465
545
  lines.push(`${I3}${resp.statusCode}: {`);
466
546
  if (hasBody) {
467
547
  lines.push(...printContentTypeLine(resp.contentType, resp.bodyType, I4));
@@ -508,29 +588,40 @@ function printOptionsBlock(ast) {
508
588
  const hasSecurity = ast.security !== void 0;
509
589
  const hasRequestHeaders = (ast.requestHeaders?.length ?? 0) > 0;
510
590
  const hasResponseHeaders = (ast.responseHeaders?.length ?? 0) > 0;
511
- if (!hasMeta && !hasServices && !hasSecurity && !hasRequestHeaders && !hasResponseHeaders) return null;
591
+ const hasBodyComments = ast.optionsComments?.body !== void 0;
592
+ if (!hasMeta && !hasServices && !hasSecurity && !hasRequestHeaders && !hasResponseHeaders && !hasBodyComments) return null;
512
593
  const lines = [
513
594
  "options {"
514
595
  ];
596
+ const body = ast.optionsComments?.body;
597
+ const emitLeading = /* @__PURE__ */ __name((scope) => {
598
+ for (const c of body?.leading?.[scope] ?? []) lines.push(`${INDENT}# ${c}`);
599
+ }, "emitLeading");
515
600
  if (hasMeta) {
601
+ emitLeading("keys");
516
602
  lines.push(`${INDENT}keys: {`);
517
603
  emitOptionsEntries(lines, ast.meta, ast.optionsComments?.keys);
518
604
  lines.push(`${INDENT}}`);
519
605
  }
520
606
  if (hasServices) {
607
+ emitLeading("services");
521
608
  lines.push(`${INDENT}services: {`);
522
609
  emitOptionsEntries(lines, ast.services, ast.optionsComments?.services);
523
610
  lines.push(`${INDENT}}`);
524
611
  }
525
612
  if (hasRequestHeaders) {
613
+ emitLeading("request");
526
614
  lines.push(...printOptionsHeaderScope("request", ast.requestHeaders));
527
615
  }
528
616
  if (hasResponseHeaders) {
617
+ emitLeading("response");
529
618
  lines.push(...printOptionsHeaderScope("response", ast.responseHeaders));
530
619
  }
531
620
  if (hasSecurity) {
621
+ emitLeading("security");
532
622
  lines.push(...printSecurity(ast.security, INDENT, INDENT + INDENT));
533
623
  }
624
+ for (const c of body?.trailing ?? []) lines.push(`${INDENT}# ${c}`);
534
625
  lines.push("}");
535
626
  return lines.join("\n");
536
627
  }
@@ -558,7 +649,7 @@ function printCk(ast, printWidth = DEFAULT_PRINT_WIDTH) {
558
649
  if (options) parts.push(options);
559
650
  for (const model of ast.models) {
560
651
  if (parts.length > 0) parts.push("");
561
- parts.push(`contract ${printModelDecl(model, printWidth)}`);
652
+ parts.push(printDeclLeadIn(model.leadingComments, model.descriptionInline ? void 0 : model.description) + `contract ${printModelDecl(model, printWidth)}`);
562
653
  }
563
654
  const emptyBlocks = [];
564
655
  const emptyIdx = {
@@ -567,11 +658,23 @@ function printCk(ast, printWidth = DEFAULT_PRINT_WIDTH) {
567
658
  for (const route of ast.routes) {
568
659
  if (parts.length > 0) parts.push("");
569
660
  const modPart = route.modifiers?.length ? `(${route.modifiers[0]})` : "";
570
- parts.push(`operation${modPart} ${printRoute(route, emptyBlocks, emptyIdx, Infinity)}`);
661
+ parts.push(printDeclLeadIn(route.leadingComments, route.description) + `operation${modPart} ${printRoute(route, emptyBlocks, emptyIdx, Infinity)}`);
571
662
  }
572
663
  return parts.join("\n") + "\n";
573
664
  }
574
665
  __name(printCk, "printCk");
666
+ function printDeclLeadIn(leadingComments, description) {
667
+ const lines = [];
668
+ if (leadingComments?.length) {
669
+ for (const c of leadingComments) lines.push(`# ${c}`);
670
+ lines.push("");
671
+ }
672
+ if (description) {
673
+ for (const line of description.split("\n")) lines.push(`# ${line}`);
674
+ }
675
+ return lines.length > 0 ? lines.join("\n") + "\n" : "";
676
+ }
677
+ __name(printDeclLeadIn, "printDeclLeadIn");
575
678
 
576
679
  // src/index.ts
577
680
  var { hardline, join } = builders;