@contractkit/prettier-plugin 0.12.2 → 0.14.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 26.17 KB
9
+ ESM dist/index.js.map 65.66 KB
10
+ ESM ⚡️ Build success in 231ms
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 4442ms
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) 923ms
7
+ ✓ tests/round-trip.test.ts (34 tests) 2390ms
8
+ ✓ formats test.ck to itself  690ms
9
+ ✓ formatting is a fixed point for source #0  1192ms
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  115 passed (115)
13
+  Start at  16:09:43
14
+  Duration  8.00s (transform 3.22s, setup 0ms, import 8.69s, tests 3.31s, 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 | 76.13 | 71.15 | 85.36 | 82.04 |
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 | 89.09 | 87.56 | 100 | 97.36 | 91-92
23
+ print-contract.ts | 45.71 | 38.23 | 50 | 46.66 | 20,52-85
24
+ ...t-operation.ts | 78.41 | 70.42 | 95 | 86.75 | ...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,54 @@
1
1
  # @contractkit/prettier-plugin-contractkit
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 85d7566: Let an operation emit more than one status, and a status serve more than one content type.
8
+
9
+ A status code could previously declare only one mime — the parser warned `Duplicate response body` and dropped the rest — and the generated router pinned both `ctx.status` and `ctx.type` to whichever response happened to be listed first with a body. An endpoint serving several formats had to declare one lying mime and let browsers sniff, and a service had no way to say which status it produced.
10
+
11
+ A status now holds every declared `mime: Type` line (`OpResponseNode.bodies`). When there is more than one, the service picks at runtime and the router sets `ctx.type` from the returned `contentType`. When an operation produces more than one status, the service returns a union discriminated on `status` and the handler switches on it, so each status writes only its own headers, mime and body. Both SDKs mirror the router: the TypeScript and Python clients return a matching union, report which mime came back, and pass the non-2xx statuses they expect to the shared fetch so a declared `304` no longer surfaces as an error. `SdkError` now takes a body type parameter, and each operation exports a `…ErrorBody` alias for the statuses that stay on the throw path.
12
+
13
+ Which statuses the service produces is derived from the declaration: **a status is emitted if it has a block, or is 2xx.** An empty block (`304: {}`) says the service returns that status carrying nothing; a bare `304:` says it is documented and something else produces it. `404(documented): { … }` is the one modifier, forcing a block-carrying status back out.
14
+
15
+ Two long-standing bugs in the same area go with it. The formatter deleted any comment written inside a `response` block — above a status code, above a mime line, above a `headers:` block, or before a closing brace — so `pnpm format` silently threw away the notes explaining why a contract looks the way it does; all four positions now round-trip. And the generated router declared a `_ZodBinary` helper for a binary _response_ body, which is a plain `Buffer` annotation with no schema behind it, leaving an unused const that tripped `noUnusedLocals` downstream; helpers are now chosen from the code that was actually generated, the same way imports already were.
16
+
17
+ **If you are already on a pre-release version, two things change under you.** A contract that declares a body on an error status — `404: { application/json: Problem }` alongside a `200` — now returns it from the service instead of throwing, and the SDK return type becomes a union; add `(documented)` to that status to keep the previous behaviour. Contracts whose error statuses are bodyless (`400:`, `404:`) are unaffected. Anything reading the AST directly should move from `OpResponseNode.contentType`/`bodyType` to `bodies`, which replaces them.
18
+
19
+ - 90d19ee: Allow a comment above `options`, and a trailing comment on an options entry.
20
+
21
+ A `#` comment above a `contract` or `operation` has always been fine, but one above the `options` keyword was a parse error — a file header is a natural thing to write, and writing it broke the build. `OptionsBlock` now owns the leading `comment*`. It lives there rather than on `Root` deliberately: on `Root` its greedy match would swallow the doc comment above a `contract` in a file that has no options block at all.
22
+
23
+ A trailing comment on a `keys`/`services` entry was worse than unsupported: it was swallowed into the value, and because an unquoted value ends at the first `}`, a comment containing one — `# interpolated as {{area}}` — closed the block early and silently mis-parsed the rest of the file. The value now ends at whitespace-then-`#`, and the comment is retained so the formatter round-trips it. A `#` with no space before it still belongs to the value, so the unquoted subpath form (`PetService: #modules/pet/pet.service.js`) is unaffected.
24
+
25
+ The TextMate grammar had the matching gap: its unquoted-value pattern accepted identifiers only, so an unquoted subpath fell through to the comment pattern and was coloured as though the parser ignored it.
26
+
27
+ An unquoted value also stays unquoted. The AST now records which entries were authored bare, so the formatter reproduces that choice instead of normalizing every value to the quoted form. The flag is formatting-only — both forms parse to the same string — and a value that could not be read back bare is still quoted, which covers values built programmatically rather than parsed.
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies [85d7566]
32
+ - Updated dependencies [90d19ee]
33
+ - @contractkit/core@0.25.0
34
+
35
+ ## 0.13.0
36
+
37
+ ### Minor Changes
38
+
39
+ - 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.
40
+
41
+ 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.
42
+
43
+ Comments may now also sit directly inside an `options { ... }` block, between its sub-blocks, where the grammar previously rejected them.
44
+
45
+ These AST fields are additive and optional; codegen plugins ignore them.
46
+
47
+ ### Patch Changes
48
+
49
+ - Updated dependencies [23e4beb]
50
+ - @contractkit/core@0.24.0
51
+
3
52
  ## 0.12.2
4
53
 
5
54
  ### 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,21 @@ 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. A status declaring several mimes on one line stays on one line too.
55
+ - **Comments inside a `response` block**, in all four positions: above a status code, above a `mime: Type` line, above a `headers:` block, and before either closing brace.
56
+ - **An empty status block.** `304: {}` and `304:` mean different things to codegen — the first says the service returns that status with no body — so neither is normalized into the other.
57
+ - **Comments in the `options` block**, including a header comment above the `options` keyword and a trailing comment on a `keys`/`services` entry.
58
+ - **Unquoted options values.** `PetService: #modules/pet/pet.service.js` stays bare and a quoted value stays quoted. Both parse to the same string, so the parser records which form was written; a value that could not be read back bare is quoted regardless.
59
+
60
+ 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.
61
+
49
62
  ## Source layout
50
63
 
51
64
  | 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, op.responsesTrailingComments) : [];
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, '\\"');
@@ -453,19 +531,29 @@ function printContentTypeLine(contentType, bodyType, lineIndent) {
453
531
  ];
454
532
  }
455
533
  __name(printContentTypeLine, "printContentTypeLine");
456
- function printResponseBlock(responses) {
534
+ function printResponseBlock(responses, trailingComments) {
457
535
  const lines = [
458
536
  `${I2}response: {`
459
537
  ];
460
538
  for (const resp of responses) {
461
- const hasBody = resp.contentType && resp.bodyType;
539
+ for (const comment of resp.leadingComments ?? []) lines.push(`${I3}# ${comment}`);
540
+ const bodies = resp.bodies;
462
541
  const hasHeaders = resp.headers && resp.headers.length > 0;
463
542
  const optOut = resp.headersOptOut;
464
- if (hasBody || hasHeaders || optOut) {
465
- lines.push(`${I3}${resp.statusCode}: {`);
466
- if (hasBody) {
467
- lines.push(...printContentTypeLine(resp.contentType, resp.bodyType, I4));
543
+ const code = resp.emit ? `${resp.statusCode}(${resp.emit})` : `${resp.statusCode}`;
544
+ const inlinable = resp.inline && bodies.length > 0 && !hasHeaders && !optOut && bodies.every((b) => b.bodyType.kind !== "inlineObject");
545
+ if (inlinable) {
546
+ const inner = bodies.map((b) => `${b.contentType}: ${printType(b.bodyType)}`).join(" ");
547
+ lines.push(`${I3}${code}: { ${inner} }`);
548
+ } else if (bodies.length === 0 && !hasHeaders && !optOut && resp.hasBlock) {
549
+ lines.push(`${I3}${code}: {}`);
550
+ } else if (bodies.length > 0 || hasHeaders || optOut || (resp.trailingComments?.length ?? 0) > 0) {
551
+ lines.push(`${I3}${code}: {`);
552
+ for (const body of bodies) {
553
+ for (const comment of body.leadingComments ?? []) lines.push(`${I4}# ${comment}`);
554
+ lines.push(...printContentTypeLine(body.contentType, body.bodyType, I4));
468
555
  }
556
+ for (const comment of resp.headersLeadingComments ?? []) lines.push(`${I4}# ${comment}`);
469
557
  if (optOut) {
470
558
  lines.push(`${I4}headers: none`);
471
559
  } else if (hasHeaders) {
@@ -477,11 +565,13 @@ function printResponseBlock(responses) {
477
565
  }
478
566
  lines.push(`${I4}}`);
479
567
  }
568
+ for (const comment of resp.trailingComments ?? []) lines.push(`${I4}# ${comment}`);
480
569
  lines.push(`${I3}}`);
481
570
  } else {
482
- lines.push(`${I3}${resp.statusCode}:`);
571
+ lines.push(`${I3}${code}:`);
483
572
  }
484
573
  }
574
+ for (const comment of trailingComments ?? []) lines.push(`${I3}# ${comment}`);
485
575
  lines.push(`${I2}}`);
486
576
  return lines;
487
577
  }
@@ -489,15 +579,19 @@ __name(printResponseBlock, "printResponseBlock");
489
579
 
490
580
  // src/print-ck.ts
491
581
  var DEFAULT_PRINT_WIDTH = 80;
492
- function quoteOptionsValue(value) {
493
- return /^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(value) ? value : `"${value}"`;
582
+ function quoteOptionsValue(value, wasUnquoted = false) {
583
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(value)) return value;
584
+ const roundTripsBare = value.length > 0 && value === value.trim() && !/[\n\r}]/.test(value) && !/[ \t]#/.test(value) && !/^["']/.test(value);
585
+ return wasUnquoted && roundTripsBare ? value : `"${value}"`;
494
586
  }
495
587
  __name(quoteOptionsValue, "quoteOptionsValue");
496
- function emitOptionsEntries(lines, entries, comments) {
588
+ function emitOptionsEntries(lines, entries, comments, unquoted) {
497
589
  const I22 = INDENT + INDENT;
590
+ const wasUnquoted = new Set(unquoted ?? []);
498
591
  for (const [key, value] of Object.entries(entries)) {
499
592
  for (const c of comments?.leading?.[key] ?? []) lines.push(`${I22}# ${c}`);
500
- lines.push(`${I22}${key}: ${quoteOptionsValue(value)}`);
593
+ const inline = comments?.inline?.[key];
594
+ lines.push(`${I22}${key}: ${quoteOptionsValue(value, wasUnquoted.has(key))}${inline !== void 0 ? ` # ${inline}` : ""}`);
501
595
  }
502
596
  for (const c of comments?.trailing ?? []) lines.push(`${I22}# ${c}`);
503
597
  }
@@ -508,29 +602,41 @@ function printOptionsBlock(ast) {
508
602
  const hasSecurity = ast.security !== void 0;
509
603
  const hasRequestHeaders = (ast.requestHeaders?.length ?? 0) > 0;
510
604
  const hasResponseHeaders = (ast.responseHeaders?.length ?? 0) > 0;
511
- if (!hasMeta && !hasServices && !hasSecurity && !hasRequestHeaders && !hasResponseHeaders) return null;
605
+ const hasBodyComments = ast.optionsComments?.body !== void 0;
606
+ if (!hasMeta && !hasServices && !hasSecurity && !hasRequestHeaders && !hasResponseHeaders && !hasBodyComments) return null;
512
607
  const lines = [
608
+ ...(ast.optionsComments?.leading ?? []).map((c) => `# ${c}`),
513
609
  "options {"
514
610
  ];
611
+ const body = ast.optionsComments?.body;
612
+ const emitLeading = /* @__PURE__ */ __name((scope) => {
613
+ for (const c of body?.leading?.[scope] ?? []) lines.push(`${INDENT}# ${c}`);
614
+ }, "emitLeading");
515
615
  if (hasMeta) {
616
+ emitLeading("keys");
516
617
  lines.push(`${INDENT}keys: {`);
517
- emitOptionsEntries(lines, ast.meta, ast.optionsComments?.keys);
618
+ emitOptionsEntries(lines, ast.meta, ast.optionsComments?.keys, ast.optionsUnquoted?.keys);
518
619
  lines.push(`${INDENT}}`);
519
620
  }
520
621
  if (hasServices) {
622
+ emitLeading("services");
521
623
  lines.push(`${INDENT}services: {`);
522
- emitOptionsEntries(lines, ast.services, ast.optionsComments?.services);
624
+ emitOptionsEntries(lines, ast.services, ast.optionsComments?.services, ast.optionsUnquoted?.services);
523
625
  lines.push(`${INDENT}}`);
524
626
  }
525
627
  if (hasRequestHeaders) {
628
+ emitLeading("request");
526
629
  lines.push(...printOptionsHeaderScope("request", ast.requestHeaders));
527
630
  }
528
631
  if (hasResponseHeaders) {
632
+ emitLeading("response");
529
633
  lines.push(...printOptionsHeaderScope("response", ast.responseHeaders));
530
634
  }
531
635
  if (hasSecurity) {
636
+ emitLeading("security");
532
637
  lines.push(...printSecurity(ast.security, INDENT, INDENT + INDENT));
533
638
  }
639
+ for (const c of body?.trailing ?? []) lines.push(`${INDENT}# ${c}`);
534
640
  lines.push("}");
535
641
  return lines.join("\n");
536
642
  }
@@ -558,7 +664,7 @@ function printCk(ast, printWidth = DEFAULT_PRINT_WIDTH) {
558
664
  if (options) parts.push(options);
559
665
  for (const model of ast.models) {
560
666
  if (parts.length > 0) parts.push("");
561
- parts.push(`contract ${printModelDecl(model, printWidth)}`);
667
+ parts.push(printDeclLeadIn(model.leadingComments, model.descriptionInline ? void 0 : model.description) + `contract ${printModelDecl(model, printWidth)}`);
562
668
  }
563
669
  const emptyBlocks = [];
564
670
  const emptyIdx = {
@@ -567,11 +673,23 @@ function printCk(ast, printWidth = DEFAULT_PRINT_WIDTH) {
567
673
  for (const route of ast.routes) {
568
674
  if (parts.length > 0) parts.push("");
569
675
  const modPart = route.modifiers?.length ? `(${route.modifiers[0]})` : "";
570
- parts.push(`operation${modPart} ${printRoute(route, emptyBlocks, emptyIdx, Infinity)}`);
676
+ parts.push(printDeclLeadIn(route.leadingComments, route.description) + `operation${modPart} ${printRoute(route, emptyBlocks, emptyIdx, Infinity)}`);
571
677
  }
572
678
  return parts.join("\n") + "\n";
573
679
  }
574
680
  __name(printCk, "printCk");
681
+ function printDeclLeadIn(leadingComments, description) {
682
+ const lines = [];
683
+ if (leadingComments?.length) {
684
+ for (const c of leadingComments) lines.push(`# ${c}`);
685
+ lines.push("");
686
+ }
687
+ if (description) {
688
+ for (const line of description.split("\n")) lines.push(`# ${line}`);
689
+ }
690
+ return lines.length > 0 ? lines.join("\n") + "\n" : "";
691
+ }
692
+ __name(printDeclLeadIn, "printDeclLeadIn");
575
693
 
576
694
  // src/index.ts
577
695
  var { hardline, join } = builders;