@contractkit/plugin-typescript 0.28.0 → 0.28.1

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.
Files changed (40) hide show
  1. package/.turbo/turbo-build$colon$ci.log +4 -4
  2. package/.turbo/turbo-test$colon$ci.log +19 -18
  3. package/CHANGELOG.md +8 -0
  4. package/dist/codegen-contract.d.ts.map +1 -1
  5. package/dist/codegen-sdk.d.ts.map +1 -1
  6. package/dist/index.js +234 -210
  7. package/dist/index.js.map +1 -1
  8. package/dist/path-utils.d.ts.map +1 -1
  9. package/dist/ts-render.d.ts +6 -0
  10. package/dist/ts-render.d.ts.map +1 -1
  11. package/package.json +2 -2
  12. package/src/codegen-contract.ts +7 -4
  13. package/src/codegen-operation.ts +4 -4
  14. package/src/codegen-plain-types.ts +17 -14
  15. package/src/codegen-sdk.ts +5 -4
  16. package/src/path-utils.ts +37 -20
  17. package/src/ts-render.ts +21 -6
  18. package/tests/codegen-contract.test.ts +4 -0
  19. package/tests/codegen-sdk.test.ts +8 -0
  20. package/tests/escaping-security.test.ts +143 -0
  21. package/coverage/base.css +0 -224
  22. package/coverage/block-navigation.js +0 -87
  23. package/coverage/clover.xml +0 -2213
  24. package/coverage/coverage-final.json +0 -9
  25. package/coverage/favicon.png +0 -0
  26. package/coverage/index.html +0 -131
  27. package/coverage/prettify.css +0 -1
  28. package/coverage/prettify.js +0 -2
  29. package/coverage/sort-arrow-sprite.png +0 -0
  30. package/coverage/sorter.js +0 -210
  31. package/coverage/src/codegen-contract.ts.html +0 -3661
  32. package/coverage/src/codegen-operation.ts.html +0 -2584
  33. package/coverage/src/codegen-plain-types.ts.html +0 -997
  34. package/coverage/src/codegen-sdk.ts.html +0 -3901
  35. package/coverage/src/index.html +0 -206
  36. package/coverage/src/index.ts.html +0 -2761
  37. package/coverage/src/path-utils.ts.html +0 -745
  38. package/coverage/src/ts-render.ts.html +0 -592
  39. package/coverage/tests/helpers.ts.html +0 -826
  40. package/coverage/tests/index.html +0 -116
package/dist/index.js CHANGED
@@ -8,6 +8,160 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync
8
8
  // src/codegen-contract.ts
9
9
  import { relative, dirname } from "path";
10
10
  import { collectTypeRefs, computeModelsWithOutput as ckComputeModelsWithOutput, collectExternalOutputRefs as ckCollectExternalOutputRefs } from "@contractkit/core";
11
+
12
+ // src/ts-render.ts
13
+ var JSON_VALUE_TYPE_DECL = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
14
+ function quoteKey(name) {
15
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
16
+ }
17
+ __name(quoteKey, "quoteKey");
18
+ function escapeJsDocLines(text) {
19
+ return text.replace(/\*\//g, "*\\/").split("\n");
20
+ }
21
+ __name(escapeJsDocLines, "escapeJsDocLines");
22
+ function escapeSingleQuoted(s) {
23
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
24
+ }
25
+ __name(escapeSingleQuoted, "escapeSingleQuoted");
26
+ function headerNameToProperty(name) {
27
+ const parts = name.split(/[-_]/).filter(Boolean);
28
+ return parts.map((p, i) => {
29
+ const lower = p.toLowerCase();
30
+ return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
31
+ }).join("");
32
+ }
33
+ __name(headerNameToProperty, "headerNameToProperty");
34
+ function renderTsType(type) {
35
+ switch (type.kind) {
36
+ case "scalar":
37
+ return renderTsScalar(type.name);
38
+ case "array": {
39
+ const inner = renderTsType(type.item);
40
+ const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
41
+ return needsParens ? `(${inner})[]` : `${inner}[]`;
42
+ }
43
+ case "tuple":
44
+ return `[${type.items.map(renderTsType).join(", ")}]`;
45
+ case "record":
46
+ return `Record<${renderTsType(type.key)}, ${renderTsType(type.value)}>`;
47
+ case "enum":
48
+ return type.values.map((v) => `'${escapeSingleQuoted(v)}'`).join(" | ");
49
+ case "literal":
50
+ return typeof type.value === "string" ? `'${escapeSingleQuoted(type.value)}'` : String(type.value);
51
+ case "union":
52
+ return type.members.map(renderTsType).join(" | ");
53
+ case "discriminatedUnion":
54
+ return type.members.map(renderTsType).join(" | ");
55
+ case "intersection":
56
+ return type.members.map(renderTsType).join(" & ");
57
+ case "ref":
58
+ return type.name;
59
+ case "lazy":
60
+ return renderTsType(type.inner);
61
+ case "inlineObject":
62
+ return renderTsInlineObject(type.fields);
63
+ default:
64
+ return "unknown";
65
+ }
66
+ }
67
+ __name(renderTsType, "renderTsType");
68
+ function renderTsScalar(name) {
69
+ switch (name) {
70
+ case "string":
71
+ case "email":
72
+ case "url":
73
+ case "uuid":
74
+ return "string";
75
+ case "number":
76
+ case "int":
77
+ return "number";
78
+ case "bigint":
79
+ return "bigint";
80
+ case "boolean":
81
+ return "boolean";
82
+ case "date":
83
+ case "time":
84
+ case "datetime":
85
+ case "duration":
86
+ case "interval":
87
+ return "string";
88
+ case "null":
89
+ return "null";
90
+ case "unknown":
91
+ return "unknown";
92
+ case "object":
93
+ return "Record<string, unknown>";
94
+ case "binary":
95
+ return "Blob";
96
+ case "json":
97
+ return "JsonValue";
98
+ default: {
99
+ const _exhaustive = name;
100
+ throw new Error(`plugin-typescript: unmapped scalar '${String(_exhaustive)}' \u2014 add a case`);
101
+ }
102
+ }
103
+ }
104
+ __name(renderTsScalar, "renderTsScalar");
105
+ function renderTsInlineObject(fields) {
106
+ const entries = fields.map((f) => {
107
+ const opt = f.optional ? "?" : "";
108
+ return `${quoteKey(f.name)}${opt}: ${renderTsType(f.type)}`;
109
+ });
110
+ return `{ ${entries.join("; ")} }`;
111
+ }
112
+ __name(renderTsInlineObject, "renderTsInlineObject");
113
+ function renderInputTsType(type, modelsWithInput) {
114
+ if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type);
115
+ switch (type.kind) {
116
+ case "ref":
117
+ return modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
118
+ case "array": {
119
+ const inner = renderInputTsType(type.item, modelsWithInput);
120
+ const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
121
+ return needsParens ? `(${inner})[]` : `${inner}[]`;
122
+ }
123
+ case "intersection":
124
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" & ");
125
+ case "union":
126
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
127
+ case "discriminatedUnion":
128
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
129
+ case "inlineObject":
130
+ return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderInputTsType(f.type, modelsWithInput)}`).join("; ")} }`;
131
+ case "lazy":
132
+ return renderInputTsType(type.inner, modelsWithInput);
133
+ default:
134
+ return renderTsType(type);
135
+ }
136
+ }
137
+ __name(renderInputTsType, "renderInputTsType");
138
+ function renderOutputTsType(type, modelsWithOutput) {
139
+ if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type);
140
+ switch (type.kind) {
141
+ case "ref":
142
+ return modelsWithOutput.has(type.name) ? `${type.name}Output` : type.name;
143
+ case "array": {
144
+ const inner = renderOutputTsType(type.item, modelsWithOutput);
145
+ const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
146
+ return needsParens ? `(${inner})[]` : `${inner}[]`;
147
+ }
148
+ case "intersection":
149
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" & ");
150
+ case "union":
151
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
152
+ case "discriminatedUnion":
153
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
154
+ case "inlineObject":
155
+ return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderOutputTsType(f.type, modelsWithOutput)}`).join("; ")} }`;
156
+ case "lazy":
157
+ return renderOutputTsType(type.inner, modelsWithOutput);
158
+ default:
159
+ return renderTsType(type);
160
+ }
161
+ }
162
+ __name(renderOutputTsType, "renderOutputTsType");
163
+
164
+ // src/codegen-contract.ts
11
165
  function modeToWrapper(mode) {
12
166
  switch (mode) {
13
167
  case "strict":
@@ -56,7 +210,7 @@ function generateComments(model, outPath) {
56
210
  lines.push(` * @deprecated`);
57
211
  }
58
212
  if (model.description) {
59
- lines.push(` * ${model.description}`);
213
+ for (const l of escapeJsDocLines(model.description)) lines.push(` * ${l}`);
60
214
  }
61
215
  const relPath = outPath ? relative(dirname(outPath), model.loc.file) : model.loc.file;
62
216
  lines.push(` * generated from [${model.name}](file://./${relPath}#L${model.loc.line})`);
@@ -199,9 +353,9 @@ function generateSimpleModel(model, outPath) {
199
353
  const outputKey = applyCase(field.name, outputCase);
200
354
  if (field.optional) {
201
355
  const guard = hasInputTransform ? `data.${inputKey} != null` : `data.${inputKey} !== undefined`;
202
- lines.push(` ...(${guard} ? { ${quoteKey(outputKey)}: data.${inputKey} } : {}),`);
356
+ lines.push(` ...(${guard} ? { ${quoteKey2(outputKey)}: data.${inputKey} } : {}),`);
203
357
  } else {
204
- lines.push(` ${quoteKey(outputKey)}: data.${inputKey},`);
358
+ lines.push(` ${quoteKey2(outputKey)}: data.${inputKey},`);
205
359
  }
206
360
  }
207
361
  lines.push(`}));`);
@@ -298,7 +452,7 @@ function generateThreeSchemaModel(model, outPath, modelsWithInput, modelsWithWri
298
452
  }
299
453
  const omitClause = fieldsToOmit.size > 0 ? `.omit({ ${[
300
454
  ...fieldsToOmit
301
- ].map((f) => `${quoteKey(f)}: true`).join(", ")} })` : "";
455
+ ].map((f) => `${quoteKey2(f)}: true`).join(", ")} })` : "";
302
456
  if (bases.length > 0) {
303
457
  const { head, tail } = buildExtendChain(bases, (b) => modelsWithInput?.has(b) ? `${b}Input` : b);
304
458
  lines.push(`export const ${name}Input = ${head}${tail}${omitClause}.extend({`);
@@ -343,7 +497,7 @@ function renderFieldsAsPascalCase(fields, defaultMode) {
343
497
  expr += ".nullable()";
344
498
  }
345
499
  if (f.description) expr += `.describe("${escapeString(f.description)}")`;
346
- return `${quoteKey(pascalKey)}: ${expr},`;
500
+ return `${quoteKey2(pascalKey)}: ${expr},`;
347
501
  });
348
502
  }
349
503
  __name(renderFieldsAsPascalCase, "renderFieldsAsPascalCase");
@@ -361,7 +515,7 @@ function renderFieldsAsSnakeCase(fields, defaultMode) {
361
515
  expr += ".nullable()";
362
516
  }
363
517
  if (f.description) expr += `.describe("${escapeString(f.description)}")`;
364
- return `${quoteKey(snakeKey)}: ${expr},`;
518
+ return `${quoteKey2(snakeKey)}: ${expr},`;
365
519
  });
366
520
  }
367
521
  __name(renderFieldsAsSnakeCase, "renderFieldsAsSnakeCase");
@@ -377,7 +531,7 @@ function renderField(field, defaultMode) {
377
531
  expr += ".optional()";
378
532
  }
379
533
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
380
- lines.push(`${quoteKey(field.name)}: ${expr},`);
534
+ lines.push(`${quoteKey2(field.name)}: ${expr},`);
381
535
  return lines;
382
536
  }
383
537
  __name(renderField, "renderField");
@@ -502,8 +656,10 @@ function renderScalar(s) {
502
656
  return "_ZodBinary";
503
657
  case "json":
504
658
  return "_ZodJson";
505
- default:
506
- return "z.unknown()";
659
+ default: {
660
+ const _exhaustive = s.name;
661
+ throw new Error(`plugin-typescript: unmapped scalar '${String(_exhaustive)}' \u2014 add a case`);
662
+ }
507
663
  }
508
664
  }
509
665
  __name(renderScalar, "renderScalar");
@@ -523,7 +679,7 @@ function renderRecord(r) {
523
679
  }
524
680
  __name(renderRecord, "renderRecord");
525
681
  function renderEnum(e) {
526
- const vals = e.values.map((v) => `"${v}"`).join(", ");
682
+ const vals = e.values.map((v) => `"${escapeString(v)}"`).join(", ");
527
683
  return `z.enum([${vals}])`;
528
684
  }
529
685
  __name(renderEnum, "renderEnum");
@@ -572,9 +728,9 @@ function renderInlineObject(o, parseCaseTransform, defaultMode) {
572
728
  const transformEntries = o.fields.map((f) => {
573
729
  const snakeKey = camelToSnake(f.name);
574
730
  if (f.optional) {
575
- return ` ...(data.${snakeKey} != null ? { ${quoteKey(f.name)}: data.${snakeKey} } : {}),`;
731
+ return ` ...(data.${snakeKey} != null ? { ${quoteKey2(f.name)}: data.${snakeKey} } : {}),`;
576
732
  }
577
- return ` ${quoteKey(f.name)}: data.${snakeKey},`;
733
+ return ` ${quoteKey2(f.name)}: data.${snakeKey},`;
578
734
  }).join("\n");
579
735
  return `${wrapper}({
580
736
  ${joined}
@@ -588,9 +744,9 @@ ${transformEntries}
588
744
  const transformEntries = o.fields.map((f) => {
589
745
  const pascalKey = camelToPascal(f.name);
590
746
  if (f.optional) {
591
- return ` ...(data.${pascalKey} != null ? { ${quoteKey(f.name)}: data.${pascalKey} } : {}),`;
747
+ return ` ...(data.${pascalKey} != null ? { ${quoteKey2(f.name)}: data.${pascalKey} } : {}),`;
592
748
  }
593
- return ` ${quoteKey(f.name)}: data.${pascalKey},`;
749
+ return ` ${quoteKey2(f.name)}: data.${pascalKey},`;
594
750
  }).join("\n");
595
751
  return `${wrapper}({
596
752
  ${joined}
@@ -676,7 +832,7 @@ function renderInputField(field, modelsWithInput, defaultMode) {
676
832
  expr += ".optional()";
677
833
  }
678
834
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
679
- lines.push(`${quoteKey(field.name)}: ${expr},`);
835
+ lines.push(`${quoteKey2(field.name)}: ${expr},`);
680
836
  return lines;
681
837
  }
682
838
  __name(renderInputField, "renderInputField");
@@ -736,17 +892,17 @@ function renderQueryField(field, modelsWithInput, defaultMode) {
736
892
  expr += ".optional()";
737
893
  }
738
894
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
739
- return `${quoteKey(field.name)}: ${expr},`;
895
+ return `${quoteKey2(field.name)}: ${expr},`;
740
896
  }
741
897
  __name(renderQueryField, "renderQueryField");
742
898
  function isValidIdentifier(name) {
743
899
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
744
900
  }
745
901
  __name(isValidIdentifier, "isValidIdentifier");
746
- function quoteKey(name) {
902
+ function quoteKey2(name) {
747
903
  return isValidIdentifier(name) ? name : `'${name}'`;
748
904
  }
749
- __name(quoteKey, "quoteKey");
905
+ __name(quoteKey2, "quoteKey");
750
906
  function escapeString(s) {
751
907
  return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
752
908
  }
@@ -950,149 +1106,6 @@ __name(pascalToDotCase, "pascalToDotCase");
950
1106
 
951
1107
  // src/codegen-operation.ts
952
1108
  import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType } from "@contractkit/core";
953
-
954
- // src/ts-render.ts
955
- var JSON_VALUE_TYPE_DECL = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
956
- function quoteKey2(name) {
957
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
958
- }
959
- __name(quoteKey2, "quoteKey");
960
- function headerNameToProperty(name) {
961
- const parts = name.split(/[-_]/).filter(Boolean);
962
- return parts.map((p, i) => {
963
- const lower = p.toLowerCase();
964
- return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
965
- }).join("");
966
- }
967
- __name(headerNameToProperty, "headerNameToProperty");
968
- function renderTsType(type) {
969
- switch (type.kind) {
970
- case "scalar":
971
- return renderTsScalar(type.name);
972
- case "array": {
973
- const inner = renderTsType(type.item);
974
- const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
975
- return needsParens ? `(${inner})[]` : `${inner}[]`;
976
- }
977
- case "tuple":
978
- return `[${type.items.map(renderTsType).join(", ")}]`;
979
- case "record":
980
- return `Record<${renderTsType(type.key)}, ${renderTsType(type.value)}>`;
981
- case "enum":
982
- return type.values.map((v) => `'${v}'`).join(" | ");
983
- case "literal":
984
- return typeof type.value === "string" ? `'${type.value}'` : String(type.value);
985
- case "union":
986
- return type.members.map(renderTsType).join(" | ");
987
- case "discriminatedUnion":
988
- return type.members.map(renderTsType).join(" | ");
989
- case "intersection":
990
- return type.members.map(renderTsType).join(" & ");
991
- case "ref":
992
- return type.name;
993
- case "lazy":
994
- return renderTsType(type.inner);
995
- case "inlineObject":
996
- return renderTsInlineObject(type.fields);
997
- default:
998
- return "unknown";
999
- }
1000
- }
1001
- __name(renderTsType, "renderTsType");
1002
- function renderTsScalar(name) {
1003
- switch (name) {
1004
- case "string":
1005
- case "email":
1006
- case "url":
1007
- case "uuid":
1008
- return "string";
1009
- case "number":
1010
- case "int":
1011
- return "number";
1012
- case "bigint":
1013
- return "bigint";
1014
- case "boolean":
1015
- return "boolean";
1016
- case "date":
1017
- case "datetime":
1018
- case "duration":
1019
- case "interval":
1020
- return "string";
1021
- case "null":
1022
- return "null";
1023
- case "unknown":
1024
- return "unknown";
1025
- case "object":
1026
- return "Record<string, unknown>";
1027
- case "binary":
1028
- return "Blob";
1029
- case "json":
1030
- return "JsonValue";
1031
- default:
1032
- return "unknown";
1033
- }
1034
- }
1035
- __name(renderTsScalar, "renderTsScalar");
1036
- function renderTsInlineObject(fields) {
1037
- const entries = fields.map((f) => {
1038
- const opt = f.optional ? "?" : "";
1039
- return `${quoteKey2(f.name)}${opt}: ${renderTsType(f.type)}`;
1040
- });
1041
- return `{ ${entries.join("; ")} }`;
1042
- }
1043
- __name(renderTsInlineObject, "renderTsInlineObject");
1044
- function renderInputTsType(type, modelsWithInput) {
1045
- if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type);
1046
- switch (type.kind) {
1047
- case "ref":
1048
- return modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
1049
- case "array": {
1050
- const inner = renderInputTsType(type.item, modelsWithInput);
1051
- const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
1052
- return needsParens ? `(${inner})[]` : `${inner}[]`;
1053
- }
1054
- case "intersection":
1055
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" & ");
1056
- case "union":
1057
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
1058
- case "discriminatedUnion":
1059
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
1060
- case "inlineObject":
1061
- return `{ ${type.fields.map((f) => `${quoteKey2(f.name)}${f.optional ? "?" : ""}: ${renderInputTsType(f.type, modelsWithInput)}`).join("; ")} }`;
1062
- case "lazy":
1063
- return renderInputTsType(type.inner, modelsWithInput);
1064
- default:
1065
- return renderTsType(type);
1066
- }
1067
- }
1068
- __name(renderInputTsType, "renderInputTsType");
1069
- function renderOutputTsType(type, modelsWithOutput) {
1070
- if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type);
1071
- switch (type.kind) {
1072
- case "ref":
1073
- return modelsWithOutput.has(type.name) ? `${type.name}Output` : type.name;
1074
- case "array": {
1075
- const inner = renderOutputTsType(type.item, modelsWithOutput);
1076
- const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
1077
- return needsParens ? `(${inner})[]` : `${inner}[]`;
1078
- }
1079
- case "intersection":
1080
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" & ");
1081
- case "union":
1082
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
1083
- case "discriminatedUnion":
1084
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
1085
- case "inlineObject":
1086
- return `{ ${type.fields.map((f) => `${quoteKey2(f.name)}${f.optional ? "?" : ""}: ${renderOutputTsType(f.type, modelsWithOutput)}`).join("; ")} }`;
1087
- case "lazy":
1088
- return renderOutputTsType(type.inner, modelsWithOutput);
1089
- default:
1090
- return renderTsType(type);
1091
- }
1092
- }
1093
- __name(renderOutputTsType, "renderOutputTsType");
1094
-
1095
- // src/codegen-operation.ts
1096
1109
  import { basename, dirname as dirname2, relative as relative2 } from "path";
1097
1110
  function bodyParserToken(contentType) {
1098
1111
  switch (classifyContentType(contentType)) {
@@ -1244,7 +1257,7 @@ function generateHandler(route, op, root, options) {
1244
1257
  lines.push("/**");
1245
1258
  const desc = op.description ?? route.description;
1246
1259
  if (desc) {
1247
- lines.push(` * ${desc}`);
1260
+ for (const l of escapeJsDocLines(desc)) lines.push(` * ${l}`);
1248
1261
  }
1249
1262
  const relFile = outPath ? relative2(dirname2(outPath), file) : file;
1250
1263
  lines.push(` * from [${basename(file)}](file://./${relFile}#L${op.loc.line})`);
@@ -1273,7 +1286,7 @@ function generateHandler(route, op, root, options) {
1273
1286
  middlewares.push(`bodyParserMiddleware([${tokensExpr}])`);
1274
1287
  }
1275
1288
  if (op.signature) {
1276
- const sigArgs = op.signaturePolicy ? `'${op.signature}', { policy: '${op.signaturePolicy}' }` : `'${op.signature}'`;
1289
+ const sigArgs = op.signaturePolicy ? `'${escapeSingleQuoted(op.signature)}', { policy: '${escapeSingleQuoted(op.signaturePolicy)}' }` : `'${escapeSingleQuoted(op.signature)}'`;
1277
1290
  middlewares.push(`requireSignature(${sigArgs})`);
1278
1291
  }
1279
1292
  const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
@@ -1312,7 +1325,7 @@ function generateHandler(route, op, root, options) {
1312
1325
  const serviceParts = inferService(op, route, file);
1313
1326
  const respHeaders = primaryResponse?.headers ?? [];
1314
1327
  const hasRespHeaders = respHeaders.length > 0;
1315
- const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey2(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput)}`).join("; ")} }` : "";
1328
+ const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput)}`).join("; ")} }` : "";
1316
1329
  if (primaryResponse?.bodyType) {
1317
1330
  const { annotation, prelude } = formatTypeAnnotation(primaryResponse.bodyType, options.modelsWithOutput);
1318
1331
  if (prelude) {
@@ -1953,18 +1966,19 @@ function generateMethod(route, op, file, options) {
1953
1966
  const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(primaryResponse.bodyType, modelsWithOutput);
1954
1967
  const respHeaders = primaryResponse?.headers ?? [];
1955
1968
  const hasRespHeaders = respHeaders.length > 0;
1956
- const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey2(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
1969
+ const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
1957
1970
  const returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
1958
1971
  const desc = op.description ?? route.description;
1959
1972
  if (op.name || desc) {
1960
1973
  const tags = [];
1961
1974
  if (op.name) tags.push(`@name ${op.name}`);
1962
1975
  if (desc) tags.push(`@description ${desc}`);
1963
- if (tags.length === 1) {
1964
- lines.push(` /** ${tags[0]} */`);
1976
+ const contentLines = tags.flatMap((t) => escapeJsDocLines(t));
1977
+ if (contentLines.length === 1) {
1978
+ lines.push(` /** ${contentLines[0]} */`);
1965
1979
  } else {
1966
1980
  lines.push(` /**`);
1967
- for (const tag of tags) lines.push(` * ${tag}`);
1981
+ for (const l of contentLines) lines.push(` * ${l}`);
1968
1982
  lines.push(` */`);
1969
1983
  }
1970
1984
  }
@@ -2040,7 +2054,7 @@ function generateMethod(route, op, file, options) {
2040
2054
  }
2041
2055
  const readBodyExpr = respCategory === "text" ? `await result.text()` : respCategory === "binary" ? `await result.blob()` : `await parseJson<${dataType}>(result)`;
2042
2056
  if (hasRespHeaders) {
2043
- const headerEntries = respHeaders.map((h) => `${quoteKey2(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2057
+ const headerEntries = respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2044
2058
  if (isVoid) {
2045
2059
  lines.push(` return { headers: { ${headerEntries} } };`);
2046
2060
  } else {
@@ -2152,7 +2166,7 @@ function buildMethodParams(route, op, modelsWithInput) {
2152
2166
  }
2153
2167
  if (op.query) {
2154
2168
  if (op.query.kind === "params") {
2155
- const fields = op.query.nodes.map((p) => `${quoteKey2(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2169
+ const fields = op.query.nodes.map((p) => `${quoteKey(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2156
2170
  params.push({
2157
2171
  name: "query",
2158
2172
  type: `{ ${fields} }`,
@@ -2175,7 +2189,7 @@ function buildMethodParams(route, op, modelsWithInput) {
2175
2189
  }
2176
2190
  if (op.headers) {
2177
2191
  if (op.headers.kind === "params") {
2178
- const fields = op.headers.nodes.map((p) => `${quoteKey2(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2192
+ const fields = op.headers.nodes.map((p) => `${quoteKey(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2179
2193
  params.push({
2180
2194
  name: "customHeaders",
2181
2195
  type: `{ ${fields} }`,
@@ -2877,7 +2891,7 @@ function generateComments2(model, outPath) {
2877
2891
  lines.push(` * @deprecated`);
2878
2892
  }
2879
2893
  if (model.description) {
2880
- lines.push(` * ${model.description}`);
2894
+ for (const l of escapeJsDocLines(model.description)) lines.push(` * ${l}`);
2881
2895
  }
2882
2896
  const relPath = outPath ? relative4(dirname4(outPath), model.loc.file) : model.loc.file;
2883
2897
  lines.push(` * generated from [${model.name}](file://./${relPath}#L${model.loc.line})`);
@@ -2941,34 +2955,40 @@ function generateVisibilityModel(model, outPath, modelsWithInput, modelMap) {
2941
2955
  return lines;
2942
2956
  }
2943
2957
  __name(generateVisibilityModel, "generateVisibilityModel");
2958
+ function withFieldJsDoc(jsdocParts, line) {
2959
+ if (jsdocParts.length === 0) return line;
2960
+ const contentLines = escapeJsDocLines(jsdocParts.join(" "));
2961
+ if (contentLines.length === 1) {
2962
+ return `/** ${contentLines[0]} */
2963
+ ${line}`;
2964
+ }
2965
+ const body = contentLines.map((l) => ` * ${l}`).join("\n");
2966
+ return `/**
2967
+ ${body}
2968
+ */
2969
+ ${line}`;
2970
+ }
2971
+ __name(withFieldJsDoc, "withFieldJsDoc");
2944
2972
  function renderField2(field) {
2945
2973
  const opt = field.optional || field.default !== void 0 ? "?" : "";
2946
2974
  let typeStr = renderTsType(field.type);
2947
2975
  if (field.nullable) typeStr += " | null";
2948
- const line = `${quoteKey2(field.name)}${opt}: ${typeStr};`;
2976
+ const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
2949
2977
  const jsdocParts = [];
2950
2978
  if (field.deprecated) jsdocParts.push("@deprecated");
2951
2979
  if (field.description) jsdocParts.push(field.description);
2952
- if (jsdocParts.length > 0) {
2953
- return `/** ${jsdocParts.join(" ")} */
2954
- ${line}`;
2955
- }
2956
- return line;
2980
+ return withFieldJsDoc(jsdocParts, line);
2957
2981
  }
2958
2982
  __name(renderField2, "renderField");
2959
2983
  function renderInputField2(field, modelsWithInput) {
2960
2984
  const opt = field.optional || field.default !== void 0 ? "?" : "";
2961
2985
  let typeStr = renderInputTsType(field.type, modelsWithInput);
2962
2986
  if (field.nullable) typeStr += " | null";
2963
- const line = `${quoteKey2(field.name)}${opt}: ${typeStr};`;
2987
+ const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
2964
2988
  const jsdocParts = [];
2965
2989
  if (field.deprecated) jsdocParts.push("@deprecated");
2966
2990
  if (field.description) jsdocParts.push(field.description);
2967
- if (jsdocParts.length > 0) {
2968
- return `/** ${jsdocParts.join(" ")} */
2969
- ${line}`;
2970
- }
2971
- return line;
2991
+ return withFieldJsDoc(jsdocParts, line);
2972
2992
  }
2973
2993
  __name(renderInputField2, "renderInputField");
2974
2994
  function camelToSnake2(s) {
@@ -3011,22 +3031,26 @@ function renderOutputField(field, outputCase, modelsWithOutput) {
3011
3031
  const key = applyOutputCase(field.name, outputCase);
3012
3032
  let typeStr = renderOutputTsType(field.type, modelsWithOutput);
3013
3033
  if (field.nullable) typeStr += " | null";
3014
- const line = `${quoteKey2(key)}${opt}: ${typeStr};`;
3034
+ const line = `${quoteKey(key)}${opt}: ${typeStr};`;
3015
3035
  const jsdocParts = [];
3016
3036
  if (field.deprecated) jsdocParts.push("@deprecated");
3017
3037
  if (field.description) jsdocParts.push(field.description);
3018
- if (jsdocParts.length > 0) {
3019
- return `/** ${jsdocParts.join(" ")} */
3020
- ${line}`;
3021
- }
3022
- return line;
3038
+ return withFieldJsDoc(jsdocParts, line);
3023
3039
  }
3024
3040
  __name(renderOutputField, "renderOutputField");
3025
3041
 
3026
3042
  // src/path-utils.ts
3027
- import { resolve, join, relative as relative5, dirname as dirname5 } from "path";
3043
+ import { resolve, join, relative as relative5, dirname as dirname5, isAbsolute } from "path";
3028
3044
  import { collectTypeRefs as collectTypeRefs2, collectPublicTypeNames } from "@contractkit/core";
3029
3045
  var TEMPLATE_VAR_RE = /\{\w+\}/;
3046
+ function assertWithinBase(baseOutDir, outPath) {
3047
+ const rel = relative5(resolve(baseOutDir), resolve(outPath));
3048
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
3049
+ throw new Error(`Refusing to emit outside output directory: resolved path "${outPath}" escapes "${baseOutDir}" (check options { keys } values used in output path templates)`);
3050
+ }
3051
+ return outPath;
3052
+ }
3053
+ __name(assertWithinBase, "assertWithinBase");
3030
3054
  function resolveTemplate(template, vars) {
3031
3055
  return template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
3032
3056
  }
@@ -3065,14 +3089,14 @@ function computeOpOutPath(filePath, baseDir, output, defaultSuffix, commonRoot,
3065
3089
  ext: "ck",
3066
3090
  ...meta
3067
3091
  });
3068
- if (includesFilename(resolved)) return join(baseOutDir, resolved);
3069
- return join(baseOutDir, resolved, defaultName);
3092
+ if (includesFilename(resolved)) return assertWithinBase(baseOutDir, join(baseOutDir, resolved));
3093
+ return assertWithinBase(baseOutDir, join(baseOutDir, resolved, defaultName));
3070
3094
  }
3071
3095
  if (output) {
3072
- if (includesFilename(output)) return join(baseOutDir, output);
3073
- return join(baseOutDir, output, relDir, defaultName);
3096
+ if (includesFilename(output)) return assertWithinBase(baseOutDir, join(baseOutDir, output));
3097
+ return assertWithinBase(baseOutDir, join(baseOutDir, output, relDir, defaultName));
3074
3098
  }
3075
- return join(baseOutDir, relDir, defaultName);
3099
+ return assertWithinBase(baseOutDir, join(baseOutDir, relDir, defaultName));
3076
3100
  }
3077
3101
  __name(computeOpOutPath, "computeOpOutPath");
3078
3102
  function computeContractOutPath(filePath, baseDir, output, defaultSuffix, commonRoot, meta = {}) {
@@ -3093,14 +3117,14 @@ function computeSdkOutPath(filePath, rootDir, clientOutput, commonRoot, meta = {
3093
3117
  ext: "ck",
3094
3118
  ...meta
3095
3119
  });
3096
- if (includesFilename(resolved)) return join(baseOutDir, resolved);
3097
- return join(baseOutDir, resolved, defaultOutName);
3120
+ if (includesFilename(resolved)) return assertWithinBase(baseOutDir, join(baseOutDir, resolved));
3121
+ return assertWithinBase(baseOutDir, join(baseOutDir, resolved, defaultOutName));
3098
3122
  }
3099
3123
  if (clientOutput) {
3100
- if (includesFilename(clientOutput)) return join(baseOutDir, clientOutput);
3101
- return join(baseOutDir, clientOutput, relDir, defaultOutName);
3124
+ if (includesFilename(clientOutput)) return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput));
3125
+ return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput, relDir, defaultOutName));
3102
3126
  }
3103
- return join(baseOutDir, relDir, defaultOutName);
3127
+ return assertWithinBase(baseOutDir, join(baseOutDir, relDir, defaultOutName));
3104
3128
  }
3105
3129
  __name(computeSdkOutPath, "computeSdkOutPath");
3106
3130
  function computeSdkAreaClientOutPath(area, rootDir, clientOutput) {
@@ -3121,14 +3145,14 @@ function computeSdkAreaClientOutPath(area, rootDir, clientOutput) {
3121
3145
  subarea: ""
3122
3146
  });
3123
3147
  const cleaned = fixHiddenSegment(resolved.replace(/\/+/g, "/").replace(/^\//, ""));
3124
- if (includesFilename(cleaned)) return join(baseOutDir, cleaned);
3125
- return join(baseOutDir, cleaned, `${filename}.client.ts`);
3148
+ if (includesFilename(cleaned)) return assertWithinBase(baseOutDir, join(baseOutDir, cleaned));
3149
+ return assertWithinBase(baseOutDir, join(baseOutDir, cleaned, `${filename}.client.ts`));
3126
3150
  }
3127
3151
  if (clientOutput) {
3128
- if (includesFilename(clientOutput)) return join(baseOutDir, clientOutput);
3129
- return join(baseOutDir, clientOutput, `${filename}.client.ts`);
3152
+ if (includesFilename(clientOutput)) return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput));
3153
+ return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput, `${filename}.client.ts`));
3130
3154
  }
3131
- return join(baseOutDir, `${filename}.client.ts`);
3155
+ return assertWithinBase(baseOutDir, join(baseOutDir, `${filename}.client.ts`));
3132
3156
  }
3133
3157
  __name(computeSdkAreaClientOutPath, "computeSdkAreaClientOutPath");
3134
3158
  function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta = {}) {
@@ -3145,11 +3169,11 @@ function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta =
3145
3169
  ext: "ck",
3146
3170
  ...meta
3147
3171
  });
3148
- if (includesFilename(resolved)) return join(baseOutDir, resolved);
3149
- return join(baseOutDir, resolved, defaultOutName);
3172
+ if (includesFilename(resolved)) return assertWithinBase(baseOutDir, join(baseOutDir, resolved));
3173
+ return assertWithinBase(baseOutDir, join(baseOutDir, resolved, defaultOutName));
3150
3174
  }
3151
- if (includesFilename(typeOutput)) return join(baseOutDir, typeOutput);
3152
- return join(baseOutDir, typeOutput, relDir, defaultOutName);
3175
+ if (includesFilename(typeOutput)) return assertWithinBase(baseOutDir, join(baseOutDir, typeOutput));
3176
+ return assertWithinBase(baseOutDir, join(baseOutDir, typeOutput, relDir, defaultOutName));
3153
3177
  }
3154
3178
  __name(computeSdkTypeOutPath, "computeSdkTypeOutPath");
3155
3179
  function generateBarrelFiles(contractPaths) {