@nifrajs/cli 3.5.0 → 4.0.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.
Files changed (403) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +57 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +270 -26
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +41 -21
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +982 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +34 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts.map +1 -1
  285. package/dist/sync-manifest.js +2 -1
  286. package/dist/sync-manifest.js.map +1 -1
  287. package/dist/sync-routes.d.ts +4 -3
  288. package/dist/sync-routes.d.ts.map +1 -1
  289. package/dist/sync-routes.js +4 -3
  290. package/dist/sync-routes.js.map +1 -1
  291. package/dist/test-tool.d.ts.map +1 -1
  292. package/dist/test-tool.js +30 -11
  293. package/dist/test-tool.js.map +1 -1
  294. package/dist/types-search.d.ts.map +1 -1
  295. package/dist/types-search.js +11 -5
  296. package/dist/types-search.js.map +1 -1
  297. package/dist/upgrade.d.ts +36 -6
  298. package/dist/upgrade.d.ts.map +1 -1
  299. package/dist/upgrade.js +167 -29
  300. package/dist/upgrade.js.map +1 -1
  301. package/dist/verification-plan.d.ts.map +1 -1
  302. package/dist/verification-plan.js +4 -0
  303. package/dist/verification-plan.js.map +1 -1
  304. package/dist/verification.d.ts.map +1 -1
  305. package/dist/verification.js +45 -12
  306. package/dist/verification.js.map +1 -1
  307. package/dist/web-app-options.d.ts +19 -0
  308. package/dist/web-app-options.d.ts.map +1 -0
  309. package/dist/web-app-options.js +49 -0
  310. package/dist/web-app-options.js.map +1 -0
  311. package/dist/work-graph.d.ts.map +1 -1
  312. package/dist/work-graph.js +29 -13
  313. package/dist/work-graph.js.map +1 -1
  314. package/docs/examples.json +185 -29
  315. package/docs/llms-full.txt +3807 -580
  316. package/docs/types.json +14229 -10939
  317. package/package.json +19 -12
  318. package/src/app-files.ts +40 -0
  319. package/src/assure-hydration.ts +139 -46
  320. package/src/assure.ts +17 -2
  321. package/src/capabilities-tool.ts +35 -9
  322. package/src/cdn-check.ts +268 -0
  323. package/src/check-diagnostics.ts +17 -0
  324. package/src/check-scan.ts +358 -63
  325. package/src/check.ts +97 -62
  326. package/src/cli.ts +296 -87
  327. package/src/command-catalog.ts +374 -38
  328. package/src/config-target.ts +47 -0
  329. package/src/contracts.ts +54 -12
  330. package/src/db-audit.ts +128 -0
  331. package/src/db-child.ts +285 -0
  332. package/src/db-config.ts +224 -0
  333. package/src/db-tool.ts +690 -0
  334. package/src/dev-bun-config.ts +116 -40
  335. package/src/dev-feed-tool.ts +912 -0
  336. package/src/dev-server-client.ts +267 -0
  337. package/src/diff-tool.ts +6 -5
  338. package/src/docs-search.ts +10 -3
  339. package/src/doctor.ts +98 -15
  340. package/src/env-file.ts +78 -1
  341. package/src/fix-recipes.ts +44 -31
  342. package/src/frontend-guidance.ts +5 -5
  343. package/src/i18n-check.ts +179 -0
  344. package/src/init-agents.ts +301 -38
  345. package/src/internal/code-unit-order.ts +5 -0
  346. package/src/internal/script-kind.ts +11 -0
  347. package/src/internal/source-facts.ts +96 -22
  348. package/src/internal/typescript-import.ts +663 -68
  349. package/src/introspect.ts +90 -27
  350. package/src/learn.ts +3 -3
  351. package/src/levels-tool.ts +47 -2
  352. package/src/load.ts +140 -33
  353. package/src/manifest-tool.ts +3 -2
  354. package/src/mcp-app-tool.ts +2 -0
  355. package/src/mcp-context.ts +1 -1
  356. package/src/mcp-delegate.ts +162 -0
  357. package/src/mcp-docs-tools.ts +23 -5
  358. package/src/mcp-exec.ts +160 -132
  359. package/src/mcp-http.ts +19 -5
  360. package/src/mcp-io.ts +57 -0
  361. package/src/mcp-isolate.ts +287 -0
  362. package/src/mcp-project-child.ts +182 -0
  363. package/src/mcp-render.ts +2 -2
  364. package/src/mcp-root.ts +117 -22
  365. package/src/mcp-run.ts +104 -10
  366. package/src/mcp-ws.ts +4 -1
  367. package/src/mcp.ts +106 -64
  368. package/src/migrate-layout.ts +1121 -0
  369. package/src/openapi-tool.ts +7 -3
  370. package/src/openapi-types.ts +315 -13
  371. package/src/pipeline-report.ts +22 -18
  372. package/src/port.ts +53 -30
  373. package/src/project-facts.ts +5 -0
  374. package/src/recipes/4.0.0.ts +35 -0
  375. package/src/recipes/index.ts +2 -0
  376. package/src/release-verification.ts +74 -5
  377. package/src/review.ts +13 -8
  378. package/src/route-types.ts +133 -0
  379. package/src/rules/codes.ts +9 -0
  380. package/src/rules/data-guard.ts +230 -0
  381. package/src/rules/index.ts +18 -5
  382. package/src/rules/islands.ts +12 -4
  383. package/src/rules/legacy.ts +71 -39
  384. package/src/rules/nano.ts +18 -6
  385. package/src/rules/pages.ts +99 -0
  386. package/src/rules/routes.ts +181 -2
  387. package/src/rules/secrets.ts +174 -0
  388. package/src/rules/security.ts +198 -70
  389. package/src/rules/typescript.ts +77 -0
  390. package/src/rules/zones.ts +195 -0
  391. package/src/scaffold.ts +147 -95
  392. package/src/sdk.ts +1215 -158
  393. package/src/search-terms.ts +24 -7
  394. package/src/smoke.ts +607 -0
  395. package/src/sync-manifest.ts +2 -1
  396. package/src/sync-routes.ts +4 -3
  397. package/src/test-tool.ts +32 -11
  398. package/src/types-search.ts +11 -4
  399. package/src/upgrade.ts +205 -25
  400. package/src/verification-plan.ts +13 -0
  401. package/src/verification.ts +50 -12
  402. package/src/web-app-options.ts +57 -0
  403. package/src/work-graph.ts +27 -13
package/dist/sdk.js CHANGED
@@ -6,14 +6,34 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- /** Deterministic, dependency-free SDK generation from the backend's OpenAPI contract. */
9
+ /** Deterministic typed SDK generation from the backend's OpenAPI contract. */
10
10
  import { existsSync } from "node:fs";
11
11
  import { basename, resolve } from "node:path";
12
12
  import { toOpenAPI } from "@nifrajs/schema/openapi";
13
+ import { BACKEND_APP_FILE } from "./app-files.js";
14
+ import { codeUnitOrder } from "./internal/code-unit-order.js";
15
+ export class SdkGenerationError extends Error {
16
+ issues;
17
+ constructor(issues) {
18
+ super("[nifra] strict SDK generation found " +
19
+ issues.length +
20
+ " opaque contract " +
21
+ (issues.length === 1 ? "portion" : "portions") +
22
+ ":\n" +
23
+ issues.map((issue) => `- ${issue.operation} ${issue.location}: ${issue.reason}`).join("\n"));
24
+ this.name = "SdkGenerationError";
25
+ this.issues = Object.freeze([...issues]);
26
+ }
27
+ }
13
28
  const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
29
+ const SUCCESS_STATUS = /^[23][0-9]{2}$/;
30
+ const ERROR_STATUS = /^[45][0-9]{2}$/;
31
+ const recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
32
+ ? value
33
+ : undefined;
14
34
  function operations(document) {
15
35
  const result = [];
16
- for (const [path, item] of Object.entries(document.paths).sort(([a], [b]) => a.localeCompare(b))) {
36
+ for (const [path, item] of Object.entries(document.paths).sort(([a], [b]) => codeUnitOrder(a, b))) {
17
37
  for (const method of HTTP_METHODS) {
18
38
  const operation = item[method];
19
39
  if (operation !== null && typeof operation === "object") {
@@ -24,14 +44,60 @@ function operations(document) {
24
44
  return result;
25
45
  }
26
46
  function identifier(value, fallback) {
27
- const startsWithDigit = /^\d/.test(value);
28
47
  const normalized = value
29
48
  .replace(/[^A-Za-z0-9_]+/g, "_")
30
49
  .replace(/_+/g, "_")
31
50
  .replace(/^_+|_+$/g, "");
32
- const safe = startsWithDigit ? `_${normalized}` : normalized;
51
+ const safe = /^\d/.test(normalized) ? `_${normalized}` : normalized;
33
52
  return safe === "" || safe === "_" ? fallback : safe;
34
53
  }
54
+ const PYTHON_KEYWORDS = new Set([
55
+ "and",
56
+ "as",
57
+ "assert",
58
+ "async",
59
+ "await",
60
+ "break",
61
+ "case",
62
+ "class",
63
+ "continue",
64
+ "def",
65
+ "del",
66
+ "elif",
67
+ "else",
68
+ "except",
69
+ "False",
70
+ "finally",
71
+ "for",
72
+ "from",
73
+ "global",
74
+ "if",
75
+ "import",
76
+ "in",
77
+ "is",
78
+ "lambda",
79
+ "None",
80
+ "nonlocal",
81
+ "not",
82
+ "or",
83
+ "pass",
84
+ "raise",
85
+ "return",
86
+ "True",
87
+ "try",
88
+ "while",
89
+ "with",
90
+ "yield",
91
+ ]);
92
+ function pythonIdentifier(value, fallback) {
93
+ const result = identifier(value, fallback);
94
+ return PYTHON_KEYWORDS.has(result) ? `${result}_` : result;
95
+ }
96
+ function pascal(value, fallback) {
97
+ const words = identifier(value, fallback).split("_").filter(Boolean);
98
+ const result = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
99
+ return result === "" ? fallback : result;
100
+ }
35
101
  function operationName(operation, method, path) {
36
102
  return identifier(operation.operationId ?? `${method}_${path}`, `${method}_request`);
37
103
  }
@@ -46,184 +112,906 @@ function uniqueNames(items) {
46
112
  function pathParameters(path) {
47
113
  return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]).filter(Boolean);
48
114
  }
49
- function pythonSdk(document) {
115
+ function firstSchema(container) {
116
+ return Object.values(container?.content ?? {})[0]?.schema;
117
+ }
118
+ function responseSchema(response) {
119
+ return firstSchema(response);
120
+ }
121
+ function schemaRef(schema) {
122
+ const record = recordOf(schema);
123
+ return typeof record?.$ref === "string" ? record.$ref : undefined;
124
+ }
125
+ function schemaType(schema) {
126
+ const record = recordOf(schema);
127
+ return typeof record?.type === "string" ? record.type : undefined;
128
+ }
129
+ function schemaProperties(schema) {
130
+ const properties = recordOf(recordOf(schema)?.properties);
131
+ return properties;
132
+ }
133
+ function schemaRequired(schema) {
134
+ const required = recordOf(schema)?.required;
135
+ return new Set(Array.isArray(required)
136
+ ? required.filter((value) => typeof value === "string")
137
+ : []);
138
+ }
139
+ function schemaUnion(schema) {
140
+ const record = recordOf(schema);
141
+ for (const key of ["anyOf", "oneOf"]) {
142
+ const value = record?.[key];
143
+ if (Array.isArray(value) &&
144
+ value.every((item) => typeof item === "boolean" || recordOf(item) !== undefined)) {
145
+ return value;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+ function schemaIsNullable(schema) {
151
+ return schemaUnion(schema)?.some((item) => schemaType(item) === "null") ?? false;
152
+ }
153
+ function isObjectModel(schema) {
154
+ const record = recordOf(schema);
155
+ const type = schemaType(schema);
156
+ if (type !== "object" && schemaProperties(schema) === undefined)
157
+ return false;
158
+ const additional = record?.additionalProperties;
159
+ return additional === undefined || additional === false;
160
+ }
161
+ function operationLabel(entry) {
162
+ return `${entry.method.toUpperCase()} ${entry.path}`;
163
+ }
164
+ class SchemaCatalog {
165
+ models = new Map();
166
+ componentNames = new Map();
167
+ namesBySchema = new Map();
168
+ usedNames = new Set();
169
+ constructor(components = {}) {
170
+ for (const name of Object.keys(components).sort()) {
171
+ this.componentNames.set(name, this.uniqueName(pascal(name, "Model")));
172
+ }
173
+ for (const [name, schema] of Object.entries(components)) {
174
+ const className = this.componentNames.get(name);
175
+ if (className !== undefined)
176
+ this.models.set(className, schema);
177
+ }
178
+ }
179
+ uniqueName(preferred) {
180
+ let name = preferred || "Model";
181
+ let suffix = 2;
182
+ while (this.usedNames.has(name))
183
+ name = `${preferred}_${suffix++}`;
184
+ this.usedNames.add(name);
185
+ return name;
186
+ }
187
+ model(schema, hint) {
188
+ const known = this.namesBySchema.get(schema);
189
+ if (known !== undefined)
190
+ return known;
191
+ const name = this.uniqueName(pascal(hint, "Model"));
192
+ this.namesBySchema.set(schema, name);
193
+ this.models.set(name, schema);
194
+ return name;
195
+ }
196
+ refName(ref) {
197
+ const prefix = "#/components/schemas/";
198
+ return ref.startsWith(prefix) ? this.componentNames.get(ref.slice(prefix.length)) : undefined;
199
+ }
200
+ }
201
+ class OpaqueTracker {
202
+ issues = [];
203
+ add(operation, location, reason) {
204
+ this.issues.push({ operation, location, reason });
205
+ }
206
+ }
207
+ function literal(value) {
208
+ if (typeof value === "string")
209
+ return JSON.stringify(value);
210
+ if (typeof value === "number" || typeof value === "boolean")
211
+ return String(value);
212
+ return "None";
213
+ }
214
+ function pythonType(schema, hint, catalog, tracker, operation, location) {
215
+ if (schema === undefined) {
216
+ tracker.add(operation, location, "schema is not declared");
217
+ return "Any";
218
+ }
219
+ if (typeof schema === "boolean") {
220
+ if (schema)
221
+ tracker.add(operation, location, "boolean true schema has no usable static type");
222
+ return schema ? "Any" : "Never";
223
+ }
224
+ const ref = schemaRef(schema);
225
+ if (ref !== undefined) {
226
+ const name = catalog.refName(ref);
227
+ if (name !== undefined)
228
+ return name;
229
+ tracker.add(operation, location, `unresolved schema reference ${ref}`);
230
+ return "Any";
231
+ }
232
+ const union = schemaUnion(schema);
233
+ if (union !== undefined) {
234
+ const members = union
235
+ .filter((item) => schemaType(item) !== "null")
236
+ .map((item, index) => pythonType(item, hint + (index + 1), catalog, tracker, operation, location));
237
+ if (members.length === 0)
238
+ return "None";
239
+ const rendered = [...new Set(members)].join(" | ");
240
+ return schemaIsNullable(schema) && !rendered.includes("None") ? `${rendered} | None` : rendered;
241
+ }
242
+ const properties = schemaProperties(schema);
243
+ const type = schemaType(schema);
244
+ if (type === "object" || properties !== undefined) {
245
+ if (properties === undefined &&
246
+ recordOf(schema)?.additionalProperties !== undefined &&
247
+ recordOf(schema)?.additionalProperties !== false) {
248
+ const extra = recordOf(schema)?.additionalProperties;
249
+ const extraType = typeof extra === "boolean"
250
+ ? "Any"
251
+ : pythonType(extra, `${hint}Value`, catalog, tracker, operation, location);
252
+ return `dict[str, ${extraType}]`;
253
+ }
254
+ return catalog.model(schema, hint);
255
+ }
256
+ if (type === "array") {
257
+ const item = recordOf(schema)?.items;
258
+ return ("list[" +
259
+ pythonType(item, `${hint}Item`, catalog, tracker, operation, `${location}.items`) +
260
+ "]");
261
+ }
262
+ if (type === "string") {
263
+ const enumValues = recordOf(schema)?.enum;
264
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
265
+ return `Literal[${enumValues.map(literal).join(", ")}]`;
266
+ }
267
+ return "str";
268
+ }
269
+ if (type === "integer")
270
+ return "int";
271
+ if (type === "number")
272
+ return "float";
273
+ if (type === "boolean")
274
+ return "bool";
275
+ if (type === "null")
276
+ return "None";
277
+ const constant = recordOf(schema)?.const;
278
+ if (constant !== undefined)
279
+ return `Literal[${literal(constant)}]`;
280
+ const allOf = recordOf(schema)?.allOf;
281
+ if (Array.isArray(allOf) && allOf.length > 0) {
282
+ return pythonType(allOf[0], hint, catalog, tracker, operation, location);
283
+ }
284
+ tracker.add(operation, location, "schema shape is opaque or unsupported");
285
+ return "Any";
286
+ }
287
+ function pythonModelFields(schema, name, catalog, tracker) {
288
+ const properties = schemaProperties(schema);
289
+ if (properties === undefined)
290
+ return " pass";
291
+ const required = schemaRequired(schema);
292
+ const used = new Set();
293
+ const fields = [];
294
+ for (const [jsonName, property] of Object.entries(properties)) {
295
+ let fieldName = pythonIdentifier(jsonName, "field");
296
+ while (used.has(fieldName))
297
+ fieldName += "_";
298
+ used.add(fieldName);
299
+ let type = pythonType(property, name + pascal(jsonName, "Field"), catalog, tracker, "schema", `model ${name}.${jsonName}`);
300
+ const optional = !required.has(jsonName);
301
+ if (optional && !type.includes("None"))
302
+ type += " | None";
303
+ if (fieldName === jsonName) {
304
+ fields.push(` ${fieldName}: ${type}${optional ? " = None" : ""}`);
305
+ }
306
+ else {
307
+ const metadata = `{"json_name": ${JSON.stringify(jsonName)}}`;
308
+ fields.push(" " +
309
+ fieldName +
310
+ ": " +
311
+ type +
312
+ " = field(" +
313
+ (optional ? "default=None, " : "") +
314
+ "metadata=" +
315
+ metadata +
316
+ ")");
317
+ }
318
+ }
319
+ return fields.length === 0 ? " pass" : fields.join("\n");
320
+ }
321
+ function renderPythonModels(catalog, tracker) {
322
+ const rendered = [];
323
+ let index = 0;
324
+ while (index < catalog.models.size) {
325
+ const entries = [...catalog.models.entries()];
326
+ const [name, schema] = entries[index++];
327
+ if (isObjectModel(schema)) {
328
+ rendered.push("@dataclass(frozen=True)\nclass " +
329
+ name +
330
+ ":\n" +
331
+ pythonModelFields(schema, name, catalog, tracker));
332
+ }
333
+ else {
334
+ rendered.push(name +
335
+ ": TypeAlias = " +
336
+ pythonType(schema, name, catalog, tracker, "schema", `model ${name}`));
337
+ }
338
+ }
339
+ return rendered.join("\n\n");
340
+ }
341
+ function pythonErrorAliases(entries, names, catalog, tracker) {
342
+ const aliases = [];
343
+ entries.forEach((entry, index) => {
344
+ const types = [];
345
+ for (const [status, response] of Object.entries(entry.operation.responses ?? {}).sort(([a], [b]) => codeUnitOrder(a, b))) {
346
+ if (!ERROR_STATUS.test(status))
347
+ continue;
348
+ const schema = responseSchema(response);
349
+ types.push(schema === undefined
350
+ ? "None"
351
+ : pythonType(schema, `${pascal(names[index], "Request")}Error${status}`, catalog, tracker, operationLabel(entry), `response ${status}`));
352
+ }
353
+ if (types.length > 0) {
354
+ aliases.push(`type ${pascal(names[index], "Request")}Error = ${[...new Set(types)].join(" | ")}`);
355
+ }
356
+ });
357
+ return aliases.join("\n");
358
+ }
359
+ function pythonSdk(document, options) {
50
360
  const entries = operations(document);
51
361
  const names = uniqueNames(entries.map(({ method, path, operation }) => operationName(operation, method, path)));
362
+ const catalog = new SchemaCatalog(document.components?.schemas);
363
+ const tracker = new OpaqueTracker();
52
364
  const methods = [];
53
- entries.forEach(({ method, path, operation }, index) => {
365
+ entries.forEach((entry, index) => {
366
+ const { method, path, operation } = entry;
54
367
  const name = names[index];
55
- const pathParams = pathParameters(path);
56
- const queryParams = (operation.parameters ?? []).filter((parameter) => parameter.in === "query");
57
- const used = new Set(["self", "body", ...pathParams]);
58
- const queryNames = queryParams.map((parameter) => {
59
- let value = identifier(parameter.name, "query");
368
+ const label = operationLabel(entry);
369
+ const pathNames = pathParameters(path);
370
+ const pathDefinitions = pathNames.map((value) => (operation.parameters ?? []).find((parameter) => parameter.in === "path" && parameter.name === value));
371
+ const queryParameters = (operation.parameters ?? []).filter((parameter) => parameter.in === "query");
372
+ const used = new Set(["self", "body", "headers", ...pathNames]);
373
+ const queryNames = queryParameters.map((parameter) => {
374
+ let value = pythonIdentifier(parameter.name, "query");
60
375
  while (used.has(value))
61
376
  value += "_";
62
377
  used.add(value);
63
378
  return value;
64
379
  });
65
- const args = pathParams.map((parameter) => `${identifier(parameter, "param")}: Any`);
66
- const keywordArgs = queryParams.map((parameter, queryIndex) => {
67
- const required = parameter.required ? "" : " = None";
68
- return `${queryNames[queryIndex]}: Any${required}`;
380
+ const required = [];
381
+ const optional = [];
382
+ queryParameters.forEach((parameter, queryIndex) => {
383
+ const type = pythonType(parameter.schema, `${pascal(name, "Request")}Query${pascal(parameter.name, "Value")}`, catalog, tracker, label, `query ${parameter.name}`);
384
+ const value = `${queryNames[queryIndex]}: ${parameter.required ? type : `${type} | None`}`;
385
+ if (parameter.required)
386
+ required.push(value);
387
+ else
388
+ optional.push(`${value} = None`);
69
389
  });
70
- if (operation.requestBody !== undefined)
71
- keywordArgs.push("body: Any = None");
72
- const signature = [...args, ...(keywordArgs.length > 0 ? ["*", ...keywordArgs] : [])].join(", ");
390
+ const bodySchema = firstSchema(operation.requestBody);
391
+ if (operation.requestBody !== undefined) {
392
+ const bodyType = pythonType(bodySchema, `${pascal(name, "Request")}Body`, catalog, tracker, label, "request body");
393
+ if (operation.requestBody.required === true)
394
+ required.push(`body: ${bodyType}`);
395
+ else
396
+ optional.push(`body: ${bodyType} = None`);
397
+ }
398
+ optional.push("headers: Mapping[str, str] | None = None");
399
+ const signature = [
400
+ ...pathDefinitions.map((parameter, parameterIndex) => {
401
+ const type = pythonType(parameter?.schema, `${pascal(name, "Request")}Path${parameterIndex + 1}`, catalog, tracker, label, `path ${pathNames[parameterIndex]}`);
402
+ return `${pythonIdentifier(pathNames[parameterIndex], "param")}: ${type}`;
403
+ }),
404
+ "*",
405
+ ...required,
406
+ ...optional,
407
+ ].join(", ");
73
408
  const query = queryNames.length === 0
74
409
  ? "None"
75
- : `{${queryNames.map((value) => `${JSON.stringify(value)}: ${value}`).join(", ")}}`;
76
- const paths = pathParams.length === 0
410
+ : "{" +
411
+ queryNames
412
+ .map((value, queryIndex) => `${JSON.stringify(queryParameters[queryIndex].name)}: ${value}`)
413
+ .join(", ") +
414
+ "}";
415
+ const paths = pathNames.length === 0
416
+ ? "None"
417
+ : "{" +
418
+ pathNames
419
+ .map((value) => `${JSON.stringify(value)}: ${pythonIdentifier(value, "param")}`)
420
+ .join(", ") +
421
+ "}";
422
+ const success = Object.entries(operation.responses ?? {}).find(([status]) => SUCCESS_STATUS.test(status));
423
+ const responseType = success === undefined
77
424
  ? "None"
78
- : `{${pathParams.map((value) => `${JSON.stringify(value)}: ${identifier(value, "param")}`).join(", ")}}`;
79
- const body = operation.requestBody === undefined ? "None" : "body";
80
- methods.push(` def ${name}(self${signature ? `, ${signature}` : ""}) -> Any:\n` +
81
- ` return self._request(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(path)}, path_params=${paths}, query=${query}, body=${body})`);
425
+ : pythonType(responseSchema(success[1]), `${pascal(name, "Request")}Response`, catalog, tracker, label, `response ${success[0]}`);
426
+ const errorType = Object.keys(operation.responses ?? {}).some((status) => ERROR_STATUS.test(status))
427
+ ? `${pascal(name, "Request")}Error`
428
+ : "Any";
429
+ methods.push(" def " +
430
+ name +
431
+ "(self" +
432
+ (signature ? `, ${signature}` : "") +
433
+ ") -> " +
434
+ responseType +
435
+ ":\n" +
436
+ " return self._request(" +
437
+ JSON.stringify(method.toUpperCase()) +
438
+ ", " +
439
+ JSON.stringify(path) +
440
+ ", path_params=" +
441
+ paths +
442
+ ", query=" +
443
+ query +
444
+ ", body=" +
445
+ (operation.requestBody === undefined ? "None" : "body") +
446
+ ", headers=headers, response_type=" +
447
+ responseType +
448
+ ", error_type=" +
449
+ errorType +
450
+ ")");
82
451
  });
83
- return `# Generated by nifra. Regenerate with: nifra sdk --lang python
84
- from __future__ import annotations
85
-
86
- import json
87
- import urllib.error
88
- import urllib.parse
89
- import urllib.request
90
- from typing import Any, Mapping
91
-
92
-
93
- class NifraApiError(RuntimeError):
94
- def __init__(self, status: int, body: Any):
95
- super().__init__(f"Nifra API request failed with HTTP {status}")
96
- self.status = status
97
- self.body = body
98
-
99
-
100
- class Client:
101
- def __init__(self, base_url: str, headers: Mapping[str, str] | None = None, timeout: float = 30.0):
102
- self.base_url = base_url.rstrip("/")
103
- self.headers = dict(headers or {})
104
- self.timeout = timeout
105
-
106
- def _request(self, method: str, path: str, *, path_params: Mapping[str, Any] | None, query: Mapping[str, Any] | None, body: Any) -> Any:
107
- route = path
108
- for key, value in (path_params or {}).items():
109
- route = route.replace("{" + key + "}", urllib.parse.quote(str(value), safe=""))
110
- params = [(key, value) for key, value in (query or {}).items() if value is not None]
111
- url = self.base_url + route
112
- if params:
113
- url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params, doseq=True)
114
- payload = None if body is None else json.dumps(body).encode("utf-8")
115
- request = urllib.request.Request(url, data=payload, method=method, headers=self.headers)
116
- if payload is not None:
117
- request.add_header("Content-Type", "application/json")
118
- try:
119
- with urllib.request.urlopen(request, timeout=self.timeout) as response:
120
- raw = response.read()
121
- return json.loads(raw) if raw else None
122
- except urllib.error.HTTPError as error:
123
- raw = error.read()
124
- try:
125
- value = json.loads(raw) if raw else None
126
- except json.JSONDecodeError:
127
- value = raw.decode("utf-8", errors="replace")
128
- raise NifraApiError(error.code, value) from error
129
-
130
- ${methods.length === 0 ? " pass" : methods.join("\n\n")}
131
- `;
132
- }
133
- function goExportName(value) {
134
- const name = identifier(value, "Request");
135
- return name[0].toUpperCase() + name.slice(1);
136
- }
137
- function goSdk(document) {
452
+ const aliases = pythonErrorAliases(entries, names, catalog, tracker);
453
+ const models = renderPythonModels(catalog, tracker);
454
+ if (options.strict === true && tracker.issues.length > 0)
455
+ throw new SdkGenerationError(tracker.issues);
456
+ return [
457
+ "# Generated by nifra. Regenerate with: nifra sdk --lang python",
458
+ "from __future__ import annotations",
459
+ "",
460
+ "import json",
461
+ "import types",
462
+ "import urllib.error",
463
+ "import urllib.parse",
464
+ "import urllib.request",
465
+ "from dataclasses import dataclass, field, fields, is_dataclass",
466
+ "from typing import Any, Generic, Literal, Mapping, Never, TypeAlias, TypeVar, get_args, get_origin, get_type_hints",
467
+ "",
468
+ 'T = TypeVar("T")',
469
+ "",
470
+ "class NifraApiError(RuntimeError, Generic[T]):",
471
+ " def __init__(self, status: int, body: T):",
472
+ ' super().__init__(f"Nifra API request failed with HTTP {status}")',
473
+ " self.status = status",
474
+ " self.body = body",
475
+ "",
476
+ "def _decode(value: Any, target: Any) -> Any:",
477
+ " if target is None or target is Any or target is object:",
478
+ " return value",
479
+ " origin = get_origin(target)",
480
+ " args = get_args(target)",
481
+ " if origin in (list, tuple, set):",
482
+ " item = args[0] if args else Any",
483
+ " values = [_decode(item_value, item) for item_value in (value or [])]",
484
+ " return values if origin is list else tuple(values) if origin is tuple else set(values)",
485
+ " if origin is dict:",
486
+ " key_type, value_type = args if len(args) == 2 else (str, Any)",
487
+ " return {_decode(key, key_type): _decode(item, value_type) for key, item in (value or {}).items()}",
488
+ ' if origin in (types.UnionType, getattr(__import__("typing"), "Union", object)):',
489
+ " for option in args:",
490
+ " if option is type(None) and value is None:",
491
+ " return None",
492
+ " if option is type(None):",
493
+ " continue",
494
+ " try:",
495
+ " return _decode(value, option)",
496
+ " except (TypeError, ValueError, KeyError):",
497
+ " pass",
498
+ " return value",
499
+ " if isinstance(target, type) and is_dataclass(target) and isinstance(value, dict):",
500
+ " hints = get_type_hints(target)",
501
+ " result = {}",
502
+ " for item in fields(target):",
503
+ ' json_name = item.metadata.get("json_name", item.name)',
504
+ " if json_name in value:",
505
+ " result[item.name] = _decode(value[json_name], hints.get(item.name, Any))",
506
+ " return target(**result)",
507
+ " if target in (str, int, float, bool) and value is not None:",
508
+ " return target(value)",
509
+ " return value",
510
+ "",
511
+ models || "# No object models were found.",
512
+ "",
513
+ aliases || "# No typed error bodies were declared.",
514
+ "",
515
+ "class Client:",
516
+ " def __init__(self, base_url: str, headers: Mapping[str, str] | None = None, timeout: float = 30.0):",
517
+ ' self.base_url = base_url.rstrip("/")',
518
+ " self.headers = dict(headers or {})",
519
+ " self.timeout = timeout",
520
+ "",
521
+ " def _request(self, method: str, path: str, *, path_params: Mapping[str, Any] | None, query: Mapping[str, Any] | None, body: Any, headers: Mapping[str, str] | None, response_type: Any, error_type: Any) -> Any:",
522
+ " route = path",
523
+ " for key, value in (path_params or {}).items():",
524
+ ' route = route.replace("{" + key + "}", urllib.parse.quote(str(value), safe=""))',
525
+ " # A `.` or `..` segment is a step to another path, never a value: anything between this",
526
+ " # client and the server may resolve it, so the call is refused instead of sent elsewhere.",
527
+ ' if any(segment in (".", "..") for segment in route.split("/")):',
528
+ " raise ValueError(\"a path parameter cannot make a '.' or '..' path segment\")",
529
+ " params = [(key, value) for key, value in (query or {}).items() if value is not None]",
530
+ " url = self.base_url + route",
531
+ " if params:",
532
+ ' url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params, doseq=True)',
533
+ ' payload = None if body is None else json.dumps(body).encode("utf-8")',
534
+ " request = urllib.request.Request(url, data=payload, method=method, headers={**self.headers, **dict(headers or {})})",
535
+ " if payload is not None:",
536
+ ' request.add_header("Content-Type", "application/json")',
537
+ " try:",
538
+ " with urllib.request.urlopen(request, timeout=self.timeout) as response:",
539
+ " raw = response.read()",
540
+ " return _decode(json.loads(raw) if raw else None, response_type)",
541
+ " except urllib.error.HTTPError as error:",
542
+ " raw = error.read()",
543
+ " try:",
544
+ " value = json.loads(raw) if raw else None",
545
+ " except json.JSONDecodeError:",
546
+ ' value = raw.decode("utf-8", errors="replace")',
547
+ " raise NifraApiError(error.code, _decode(value, error_type)) from error",
548
+ "",
549
+ methods.length === 0 ? " pass" : methods.join("\n\n"),
550
+ "",
551
+ ].join("\n");
552
+ }
553
+ function goType(schema, hint, catalog, tracker, operation, location) {
554
+ if (schema === undefined) {
555
+ tracker.add(operation, location, "schema is not declared");
556
+ return "any";
557
+ }
558
+ if (typeof schema === "boolean") {
559
+ if (schema)
560
+ tracker.add(operation, location, "boolean true schema has no usable static type");
561
+ return schema ? "any" : "struct{}";
562
+ }
563
+ const ref = schemaRef(schema);
564
+ if (ref !== undefined) {
565
+ const name = catalog.refName(ref);
566
+ if (name !== undefined)
567
+ return name;
568
+ tracker.add(operation, location, `unresolved schema reference ${ref}`);
569
+ return "any";
570
+ }
571
+ const union = schemaUnion(schema);
572
+ if (union !== undefined) {
573
+ const members = union.filter((item) => schemaType(item) !== "null");
574
+ if (members.length === 1 && schemaIsNullable(schema)) {
575
+ const type = goType(members[0], hint, catalog, tracker, operation, location);
576
+ return type.startsWith("*") ? type : `*${type}`;
577
+ }
578
+ tracker.add(operation, location, "non-nullable unions have no direct Go representation");
579
+ return "any";
580
+ }
581
+ const properties = schemaProperties(schema);
582
+ const type = schemaType(schema);
583
+ if (type === "object" || properties !== undefined) {
584
+ if (properties === undefined &&
585
+ recordOf(schema)?.additionalProperties !== undefined &&
586
+ recordOf(schema)?.additionalProperties !== false) {
587
+ const extra = recordOf(schema)?.additionalProperties;
588
+ const extraType = typeof extra === "boolean"
589
+ ? "any"
590
+ : goType(extra, `${hint}Value`, catalog, tracker, operation, location);
591
+ return `map[string]${extraType}`;
592
+ }
593
+ return catalog.model(schema, hint);
594
+ }
595
+ if (type === "array") {
596
+ return ("[]" +
597
+ goType(recordOf(schema)?.items, `${hint}Item`, catalog, tracker, operation, `${location}.items`));
598
+ }
599
+ if (type === "string")
600
+ return "string";
601
+ if (type === "integer")
602
+ return "int64";
603
+ if (type === "number")
604
+ return "float64";
605
+ if (type === "boolean")
606
+ return "bool";
607
+ if (type === "null")
608
+ return "struct{}";
609
+ const allOf = recordOf(schema)?.allOf;
610
+ if (Array.isArray(allOf) && allOf.length > 0)
611
+ return goType(allOf[0], hint, catalog, tracker, operation, location);
612
+ tracker.add(operation, location, "schema shape is opaque or unsupported");
613
+ return "any";
614
+ }
615
+ const GO_KEYWORDS = new Set([
616
+ "break",
617
+ "default",
618
+ "func",
619
+ "interface",
620
+ "select",
621
+ "case",
622
+ "defer",
623
+ "go",
624
+ "map",
625
+ "struct",
626
+ "chan",
627
+ "else",
628
+ "goto",
629
+ "package",
630
+ "switch",
631
+ "const",
632
+ "fallthrough",
633
+ "if",
634
+ "range",
635
+ "type",
636
+ "continue",
637
+ "for",
638
+ "import",
639
+ "return",
640
+ "var",
641
+ ]);
642
+ // encoding/json reads a tag name made only of letters, decimal digits and this punctuation. A key
643
+ // with a quote, backslash, backtick, comma or control character has no struct-tag spelling, and
644
+ // writing it raw would end the tag (or the struct) early.
645
+ const GO_JSON_TAG_NAME = /^[\p{L}\p{Nd}!#$%&()*+\-./:;<=>?@[\]^_{|}~ ]+$/u;
646
+ function goFieldName(value) {
647
+ const result = pascal(value, "Field");
648
+ return GO_KEYWORDS.has(result.toLowerCase()) ? `${result}Value` : result;
649
+ }
650
+ function goModelFields(schema, name, catalog, tracker) {
651
+ const properties = schemaProperties(schema);
652
+ if (properties === undefined)
653
+ return "";
654
+ const required = schemaRequired(schema);
655
+ const used = new Set();
656
+ const tag = String.fromCharCode(96);
657
+ const fields = [];
658
+ for (const [jsonName, property] of Object.entries(properties)) {
659
+ if (!GO_JSON_TAG_NAME.test(jsonName)) {
660
+ tracker.add("schema", `model ${name}.${JSON.stringify(jsonName)}`, "property name cannot be spelled as a Go struct tag");
661
+ continue;
662
+ }
663
+ let fieldName = goFieldName(jsonName);
664
+ while (used.has(fieldName))
665
+ fieldName += "Value";
666
+ used.add(fieldName);
667
+ let type = goType(property, name + pascal(jsonName, "Field"), catalog, tracker, "schema", `model ${name}.${jsonName}`);
668
+ if (!required.has(jsonName) && type !== "any" && !type.startsWith("*") && type !== "struct{}")
669
+ type = `*${type}`;
670
+ fields.push("\t" +
671
+ fieldName +
672
+ " " +
673
+ type +
674
+ " " +
675
+ tag +
676
+ 'json:"' +
677
+ jsonName +
678
+ // A bare "-" tag omits the field; "-," names a property that is literally "-".
679
+ (required.has(jsonName) ? (jsonName === "-" ? "," : "") : ",omitempty") +
680
+ '"' +
681
+ tag);
682
+ }
683
+ return fields.join("\n");
684
+ }
685
+ function renderGoModels(catalog, tracker) {
686
+ const rendered = [];
687
+ let index = 0;
688
+ while (index < catalog.models.size) {
689
+ const entries = [...catalog.models.entries()];
690
+ const [name, schema] = entries[index++];
691
+ const fields = goModelFields(schema, name, catalog, tracker);
692
+ if (isObjectModel(schema)) {
693
+ rendered.push(`type ${name} struct {\n${fields}\n}`);
694
+ }
695
+ else {
696
+ rendered.push(`type ${name} = ${goType(schema, name, catalog, tracker, "schema", `model ${name}`)}`);
697
+ }
698
+ }
699
+ return rendered.join("\n\n");
700
+ }
701
+ function goErrorInfo(entry, name, catalog, tracker) {
702
+ const errors = Object.entries(entry.operation.responses ?? {})
703
+ .filter(([status]) => ERROR_STATUS.test(status))
704
+ .sort(([a], [b]) => codeUnitOrder(a, b));
705
+ if (errors.length === 0)
706
+ return undefined;
707
+ const operationNameValue = pascal(name, "Request");
708
+ const interfaceName = `${operationNameValue}ErrorBody`;
709
+ const types = [];
710
+ const definitions = [];
711
+ const markers = [];
712
+ for (const [status, response] of errors) {
713
+ const schema = responseSchema(response);
714
+ let type = "struct{}";
715
+ if (schema !== undefined) {
716
+ type = goType(schema, `${operationNameValue}Error${status}`, catalog, tracker, operationLabel(entry), `response ${status}`);
717
+ if (type === "string" ||
718
+ type === "int64" ||
719
+ type === "float64" ||
720
+ type === "bool" ||
721
+ type.startsWith("[]") ||
722
+ type.startsWith("map[")) {
723
+ const alias = `${operationNameValue}Error${status}`;
724
+ definitions.push(`type ${alias} ${type}`);
725
+ type = alias;
726
+ }
727
+ if (type !== "any")
728
+ markers.push(`func (v ${type}) is${interfaceName}() {}`);
729
+ }
730
+ types.push({ status, type });
731
+ }
732
+ const hasUnknown = types.some((item) => item.type === "any");
733
+ const bodyInterface = hasUnknown ? "any" : interfaceName;
734
+ if (!hasUnknown) {
735
+ definitions.unshift(`type ${interfaceName} interface {\n\tis${interfaceName}()\n}`);
736
+ definitions.push(...markers);
737
+ }
738
+ definitions.unshift("type " +
739
+ operationNameValue +
740
+ "Error struct {\n\tStatus int\n\tBody " +
741
+ bodyInterface +
742
+ "\n\tCause error\n}\n\nfunc (e *" +
743
+ operationNameValue +
744
+ 'Error) Error() string {\n\treturn fmt.Sprintf("nifra API request failed with HTTP %d", e.Status)\n}');
745
+ const decoderLines = [
746
+ "func decode" +
747
+ operationNameValue +
748
+ "Error(status int, raw []byte, cause error) *" +
749
+ operationNameValue +
750
+ "Error {",
751
+ `\tout := &${operationNameValue}Error{Status: status, Cause: cause}`,
752
+ "\tswitch status {",
753
+ ];
754
+ for (const item of types) {
755
+ if (item.type === "struct{}" || item.type === "any")
756
+ continue;
757
+ decoderLines.push(`\tcase ${item.status}:`, `\t\tvar body ${item.type}`, "\t\tif len(raw) > 0 { if err := json.Unmarshal(raw, &body); err != nil { out.Cause = err } }", "\t\tout.Body = body");
758
+ }
759
+ decoderLines.push("\t}", "\treturn out", "}");
760
+ return {
761
+ name: `${operationNameValue}Error`,
762
+ definitions: definitions.join("\n\n"),
763
+ decoders: decoderLines.join("\n"),
764
+ };
765
+ }
766
+ function goQueryDefinition(entry, name, catalog, tracker) {
767
+ const parameters = (entry.operation.parameters ?? []).filter((parameter) => parameter.in === "query");
768
+ if (parameters.length === 0)
769
+ return undefined;
770
+ const type = `${pascal(name, "Request")}Query`;
771
+ const fields = [];
772
+ const values = [];
773
+ for (const parameter of parameters) {
774
+ let fieldType = goType(parameter.schema, type + pascal(parameter.name, "Value"), catalog, tracker, operationLabel(entry), `query ${parameter.name}`);
775
+ if (!parameter.required &&
776
+ fieldType !== "any" &&
777
+ !fieldType.startsWith("*") &&
778
+ fieldType !== "struct{}")
779
+ fieldType = `*${fieldType}`;
780
+ const field = goFieldName(parameter.name);
781
+ fields.push(`\t${field} ${fieldType}`);
782
+ const value = parameter.required ? `fmt.Sprint(q.${field})` : `fmt.Sprint(*q.${field})`;
783
+ values.push(parameter.required
784
+ ? `\tv.Set(${JSON.stringify(parameter.name)}, ${value})`
785
+ : "\tif q." +
786
+ field +
787
+ " != nil {\n\t\tv.Set(" +
788
+ JSON.stringify(parameter.name) +
789
+ ", " +
790
+ value +
791
+ ")\n\t}");
792
+ }
793
+ return {
794
+ type,
795
+ source: "type " +
796
+ type +
797
+ " struct {\n" +
798
+ fields.join("\n") +
799
+ "\n}\n\nfunc (q " +
800
+ type +
801
+ ") values() url.Values {\n\tv := make(url.Values)\n" +
802
+ values.join("\n") +
803
+ "\n\treturn v\n}",
804
+ };
805
+ }
806
+ function goSdk(document, options) {
138
807
  const entries = operations(document);
139
808
  const names = uniqueNames(entries.map(({ method, path, operation }) => operationName(operation, method, path)));
809
+ const catalog = new SchemaCatalog(document.components?.schemas);
810
+ const tracker = new OpaqueTracker();
140
811
  const methods = [];
141
- entries.forEach(({ method, path }, index) => {
142
- const pathParams = pathParameters(path);
143
- const args = pathParams.map((parameter) => `${identifier(parameter, "param")} string`);
144
- args.push("query url.Values", "body any");
145
- const pathMap = pathParams.length === 0
812
+ const queries = [];
813
+ const errors = [];
814
+ const decoders = [];
815
+ const errorNames = new Map();
816
+ const queryTypes = new Map();
817
+ entries.forEach((entry, index) => {
818
+ const query = goQueryDefinition(entry, names[index], catalog, tracker);
819
+ if (query !== undefined) {
820
+ queries.push(query.source);
821
+ queryTypes.set(index, query.type);
822
+ }
823
+ const error = goErrorInfo(entry, names[index], catalog, tracker);
824
+ if (error !== undefined) {
825
+ errors.push(error.definitions);
826
+ decoders.push(error.decoders);
827
+ errorNames.set(index, error.name);
828
+ }
829
+ });
830
+ entries.forEach((entry, index) => {
831
+ const { method, path, operation } = entry;
832
+ const name = names[index];
833
+ const label = operationLabel(entry);
834
+ const pathNames = pathParameters(path);
835
+ const args = [];
836
+ for (const parameterName of pathNames) {
837
+ const parameter = (operation.parameters ?? []).find((item) => item.in === "path" && item.name === parameterName);
838
+ const type = goType(parameter?.schema, `${pascal(name, "Request")}Path${pascal(parameterName, "Value")}`, catalog, tracker, label, `path ${parameterName}`);
839
+ args.push(`${goFieldName(parameterName)} ${type}`);
840
+ }
841
+ const queryType = queryTypes.get(index);
842
+ if (queryType !== undefined)
843
+ args.push(`query ${queryType}`);
844
+ const bodySchema = firstSchema(operation.requestBody);
845
+ const bodyType = operation.requestBody === undefined
846
+ ? undefined
847
+ : goType(bodySchema, `${pascal(name, "Request")}Body`, catalog, tracker, label, "request body");
848
+ if (bodyType !== undefined)
849
+ args.push(`body ${bodyType}`);
850
+ const success = Object.entries(operation.responses ?? {}).find(([status]) => SUCCESS_STATUS.test(status));
851
+ const responseType = success === undefined
852
+ ? "struct{}"
853
+ : goType(responseSchema(success[1]), `${pascal(name, "Request")}Response`, catalog, tracker, label, `response ${success[0]}`);
854
+ const errorName = errorNames.get(index);
855
+ const errorType = errorName === undefined ? "error" : `*${errorName}`;
856
+ const pathMap = pathNames.length === 0
146
857
  ? "nil"
147
- : `map[string]string{${pathParams.map((value) => `${JSON.stringify(value)}: ${identifier(value, "param")}`).join(", ")}}`;
148
- methods.push(`func (c *Client) ${goExportName(names[index])}(${args.join(", ")}) (any, error) {\n` +
149
- ` return c.request(http.Method${method[0].toUpperCase()}${method.slice(1)}, ${JSON.stringify(path)}, ${pathMap}, query, body)\n` +
150
- `}`);
858
+ : "map[string]string{" +
859
+ pathNames
860
+ .map((value) => `${JSON.stringify(value)}: fmt.Sprint(${goFieldName(value)})`)
861
+ .join(", ") +
862
+ "}";
863
+ const queryValue = queryType === undefined ? "nil" : "query.values()";
864
+ const bodyValue = bodyType === undefined ? "nil" : "body";
865
+ const zeroError = errorName === undefined ? "err" : `&${errorName}{Status: 0, Cause: err}`;
866
+ const statusError = errorName === undefined
867
+ ? "&NifraApiError{Status: status, Body: string(raw)}"
868
+ : `decode${errorName}(status, raw, nil)`;
869
+ const unmarshalError = errorName === undefined ? "err" : `&${errorName}{Status: status, Cause: err}`;
870
+ methods.push("func (c *Client) " +
871
+ pascal(name, "Request") +
872
+ "(" +
873
+ args.join(", ") +
874
+ ") (" +
875
+ responseType +
876
+ ", " +
877
+ errorType +
878
+ ") {\n" +
879
+ "\tstatus, raw, err := c.request(http.Method" +
880
+ method[0].toUpperCase() +
881
+ method.slice(1) +
882
+ ", " +
883
+ JSON.stringify(path) +
884
+ ", " +
885
+ pathMap +
886
+ ", " +
887
+ queryValue +
888
+ ", " +
889
+ bodyValue +
890
+ ")\n" +
891
+ "\tvar value " +
892
+ responseType +
893
+ "\n\tif err != nil {\n\t\treturn value, " +
894
+ zeroError +
895
+ "\n\t}\n" +
896
+ "\tif status < 200 || status >= 300 {\n\t\treturn value, " +
897
+ statusError +
898
+ "\n\t}\n" +
899
+ "\tif len(raw) > 0 {\n\t\tif err := json.Unmarshal(raw, &value); err != nil {\n\t\t\treturn value, " +
900
+ unmarshalError +
901
+ "\n\t\t}\n\t}\n\treturn value, nil\n}");
151
902
  });
152
- return `// Code generated by nifra. Regenerate with: nifra sdk --lang go
153
- package nifrasdk
154
-
155
- import (
156
- "bytes"
157
- "encoding/json"
158
- "fmt"
159
- "io"
160
- "net/http"
161
- "net/url"
162
- "strings"
163
- )
164
-
165
- type Client struct {
166
- BaseURL string
167
- HTTP *http.Client
168
- Headers http.Header
169
- }
170
-
171
- func NewClient(baseURL string) *Client {
172
- return &Client{BaseURL: strings.TrimRight(baseURL, "/"), HTTP: http.DefaultClient, Headers: make(http.Header)}
173
- }
174
-
175
- func (c *Client) request(method, path string, pathParams map[string]string, query url.Values, body any) (any, error) {
176
- route := path
177
- for key, value := range pathParams {
178
- route = strings.ReplaceAll(route, "{"+key+"}", url.PathEscape(value))
179
- }
180
- target := c.BaseURL + route
181
- parsed, err := url.Parse(target)
182
- if err != nil { return nil, err }
183
- parsed.RawQuery = query.Encode()
184
- var reader io.Reader
185
- if body != nil {
186
- payload, err := json.Marshal(body)
187
- if err != nil { return nil, err }
188
- reader = bytes.NewReader(payload)
189
- }
190
- request, err := http.NewRequest(method, parsed.String(), reader)
191
- if err != nil { return nil, err }
192
- for key, values := range c.Headers { for _, value := range values { request.Header.Add(key, value) } }
193
- if body != nil { request.Header.Set("Content-Type", "application/json") }
194
- response, err := c.HTTP.Do(request)
195
- if err != nil { return nil, err }
196
- defer response.Body.Close()
197
- raw, err := io.ReadAll(response.Body)
198
- if err != nil { return nil, err }
199
- var value any
200
- if len(raw) > 0 { if err := json.Unmarshal(raw, &value); err != nil { value = string(raw) } }
201
- if response.StatusCode < 200 || response.StatusCode >= 300 { return nil, fmt.Errorf("nifra API request failed with HTTP %d: %v", response.StatusCode, value) }
202
- return value, nil
203
- }
204
-
205
- ${methods.length === 0 ? "// No operations were found.\n" : `${methods.join("\n\n")}\n`}`;
206
- }
207
- /** Render a generated SDK. Python uses the standard library; Go uses net/http. */
208
- export function renderSdk(document, language) {
209
- return language === "python" ? pythonSdk(document) : goSdk(document);
210
- }
211
- /** Load `backend.ts`, generate an SDK, and write it to the project. */
903
+ const models = renderGoModels(catalog, tracker);
904
+ if (options.strict === true && tracker.issues.length > 0)
905
+ throw new SdkGenerationError(tracker.issues);
906
+ return [
907
+ "// Code generated by nifra. Regenerate with: nifra sdk --lang go",
908
+ "package nifrasdk",
909
+ "",
910
+ "import (",
911
+ '\t"bytes"',
912
+ '\t"encoding/json"',
913
+ '\t"fmt"',
914
+ '\t"io"',
915
+ '\t"net/http"',
916
+ '\t"net/url"',
917
+ '\t"strings"',
918
+ ")",
919
+ "",
920
+ "type Client struct {",
921
+ "\tBaseURL string",
922
+ "\tHTTP *http.Client",
923
+ "\tHeaders http.Header",
924
+ "}",
925
+ "",
926
+ "func NewClient(baseURL string) *Client {",
927
+ '\treturn &Client{BaseURL: strings.TrimRight(baseURL, "/"), HTTP: http.DefaultClient, Headers: make(http.Header)}',
928
+ "}",
929
+ "",
930
+ "type NifraApiError struct { Status int; Body string }",
931
+ 'func (e *NifraApiError) Error() string { return fmt.Sprintf("nifra API request failed with HTTP %d", e.Status) }',
932
+ "",
933
+ "func (c *Client) request(method, path string, pathParams map[string]string, query url.Values, body any) (int, []byte, error) {",
934
+ "\troute := path",
935
+ '\tfor key, value := range pathParams { route = strings.ReplaceAll(route, "{"+key+"}", url.PathEscape(value)) }',
936
+ "\t// A `.` or `..` segment is a step to another path, never a value: anything between this client",
937
+ "\t// and the server may resolve it, so the call is refused instead of sent elsewhere.",
938
+ '\tfor _, segment := range strings.Split(route, "/") { if segment == "." || segment == ".." { return 0, nil, fmt.Errorf("a path parameter cannot make a %q or %q path segment", ".", "..") } }',
939
+ "\ttarget := c.BaseURL + route",
940
+ "\tparsed, err := url.Parse(target)",
941
+ "\tif err != nil { return 0, nil, err }",
942
+ "\tif query != nil { parsed.RawQuery = query.Encode() }",
943
+ "\tvar reader io.Reader",
944
+ "\tif body != nil { payload, err := json.Marshal(body); if err != nil { return 0, nil, err }; reader = bytes.NewReader(payload) }",
945
+ "\trequest, err := http.NewRequest(method, parsed.String(), reader)",
946
+ "\tif err != nil { return 0, nil, err }",
947
+ "\tfor key, values := range c.Headers { for _, value := range values { request.Header.Add(key, value) } }",
948
+ '\tif body != nil { request.Header.Set("Content-Type", "application/json") }',
949
+ "\tresponse, err := c.HTTP.Do(request)",
950
+ "\tif err != nil { return 0, nil, err }",
951
+ "\tdefer response.Body.Close()",
952
+ "\traw, err := io.ReadAll(response.Body)",
953
+ "\treturn response.StatusCode, raw, err",
954
+ "}",
955
+ "",
956
+ models || "// No object models were found.",
957
+ "",
958
+ queries.join("\n\n") || "// No typed query models were needed.",
959
+ "",
960
+ errors.join("\n\n") || "// No typed error bodies were declared.",
961
+ "",
962
+ decoders.join("\n\n"),
963
+ "",
964
+ methods.join("\n\n") || "// No operations were found.",
965
+ "",
966
+ ].join("\n");
967
+ }
968
+ /** Render a generated SDK. Strict mode refuses opaque request/response portions. */
969
+ export function renderSdk(document, language, options = {}) {
970
+ return language === "python" ? pythonSdk(document, options) : goSdk(document, options);
971
+ }
972
+ function unsupportedTransports(backend) {
973
+ const routes = backend.routes;
974
+ if (typeof routes !== "function")
975
+ return [];
976
+ const result = [];
977
+ for (const route of routes.call(backend)) {
978
+ const method = typeof route.method === "string" ? route.method : "?";
979
+ const path = typeof route.path === "string" ? route.path : "?";
980
+ if (route.schema?.sse !== undefined) {
981
+ result.push(`${method} ${path} uses SSE; generated SDKs currently cover JSON HTTP only`);
982
+ }
983
+ }
984
+ return result;
985
+ }
986
+ /** Load backend/app.ts, generate an SDK, and write it to the project. */
212
987
  export async function runSdk(cwd, options) {
213
- const backendPath = resolve(cwd, "backend.ts");
988
+ const backendPath = resolve(cwd, BACKEND_APP_FILE);
214
989
  if (!existsSync(backendPath)) {
215
- throw new Error(`[nifra] no backend.ts in ${cwd} - SDK generation needs the API contract.`);
990
+ throw new Error(`[nifra] no ${BACKEND_APP_FILE} in ${cwd} - SDK generation needs the API contract.`);
216
991
  }
217
992
  const backend = (await import(__rewriteRelativeImportExtension(backendPath))).backend;
218
993
  if (backend === undefined)
219
- throw new Error(`[nifra] ${backendPath} does not export \`backend\`.`);
994
+ throw new Error(`[nifra] ${backendPath} does not export backend.`);
995
+ const transportIssues = unsupportedTransports(backend);
996
+ if (options.strict === true && transportIssues.length > 0) {
997
+ throw new SdkGenerationError(transportIssues.map((reason) => ({ operation: "transport", location: "SSE", reason })));
998
+ }
999
+ for (const warning of transportIssues)
1000
+ console.warn(`[nifra] SDK warning: ${warning}`);
220
1001
  const document = toOpenAPI(backend, {
221
1002
  title: `${basename(cwd)} API`,
222
1003
  version: "1.0.0",
223
1004
  });
224
1005
  const extension = options.language === "python" ? "py" : "go";
225
1006
  const out = resolve(cwd, options.out ?? `nifra_sdk.${extension}`);
226
- await Bun.write(out, renderSdk(document, options.language));
227
- console.log(`[nifra] wrote ${options.language} SDK with ${Object.keys(document.paths).length} path${Object.keys(document.paths).length === 1 ? "" : "s"} to ${out}`);
1007
+ await Bun.write(out, renderSdk(document, options.language, options));
1008
+ console.log("[nifra] wrote " +
1009
+ options.language +
1010
+ " SDK with " +
1011
+ Object.keys(document.paths).length +
1012
+ " path" +
1013
+ (Object.keys(document.paths).length === 1 ? "" : "s") +
1014
+ " to " +
1015
+ out);
228
1016
  }
229
1017
  //# sourceMappingURL=sdk.js.map