@nifrajs/cli 4.0.2 → 5.1.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 (364) hide show
  1. package/README.md +10 -2
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +9 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/assure.d.ts +12 -2
  14. package/dist/assure.d.ts.map +1 -1
  15. package/dist/assure.js +59 -8
  16. package/dist/assure.js.map +1 -1
  17. package/dist/capabilities-tool.d.ts.map +1 -1
  18. package/dist/capabilities-tool.js +36 -9
  19. package/dist/capabilities-tool.js.map +1 -1
  20. package/dist/check-diagnostics.d.ts +22 -2
  21. package/dist/check-diagnostics.d.ts.map +1 -1
  22. package/dist/check-diagnostics.js +54 -7
  23. package/dist/check-diagnostics.js.map +1 -1
  24. package/dist/check-scan.d.ts +83 -24
  25. package/dist/check-scan.d.ts.map +1 -1
  26. package/dist/check-scan.js +1516 -318
  27. package/dist/check-scan.js.map +1 -1
  28. package/dist/check.d.ts +2 -2
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +203 -70
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +9 -0
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +215 -50
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client-module.d.ts +20 -0
  37. package/dist/client-module.d.ts.map +1 -0
  38. package/dist/client-module.js +73 -0
  39. package/dist/client-module.js.map +1 -0
  40. package/dist/command-catalog.d.ts +24 -5
  41. package/dist/command-catalog.d.ts.map +1 -1
  42. package/dist/command-catalog.js +194 -45
  43. package/dist/command-catalog.js.map +1 -1
  44. package/dist/component-check.d.ts +13 -0
  45. package/dist/component-check.d.ts.map +1 -0
  46. package/dist/component-check.js +59 -0
  47. package/dist/component-check.js.map +1 -0
  48. package/dist/contract-proof.d.ts +1 -1
  49. package/dist/contract-proof.d.ts.map +1 -1
  50. package/dist/contract-proof.js +2 -24
  51. package/dist/contract-proof.js.map +1 -1
  52. package/dist/contracts.d.ts +19 -0
  53. package/dist/contracts.d.ts.map +1 -1
  54. package/dist/contracts.js +50 -3
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db-tool.d.ts.map +1 -1
  57. package/dist/db-tool.js +4 -3
  58. package/dist/db-tool.js.map +1 -1
  59. package/dist/dev-bun-config.d.ts +5 -1
  60. package/dist/dev-bun-config.d.ts.map +1 -1
  61. package/dist/dev-bun-config.js +44 -11
  62. package/dist/dev-bun-config.js.map +1 -1
  63. package/dist/dev-feed-tool.d.ts +18 -1
  64. package/dist/dev-feed-tool.d.ts.map +1 -1
  65. package/dist/dev-feed-tool.js +93 -0
  66. package/dist/dev-feed-tool.js.map +1 -1
  67. package/dist/diagnostics.d.ts +3 -0
  68. package/dist/diagnostics.d.ts.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/diff-tool.d.ts +9 -8
  71. package/dist/diff-tool.d.ts.map +1 -1
  72. package/dist/diff-tool.js +28 -24
  73. package/dist/diff-tool.js.map +1 -1
  74. package/dist/doctor.d.ts +13 -0
  75. package/dist/doctor.d.ts.map +1 -1
  76. package/dist/doctor.js +23 -15
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/env-file.d.ts +6 -0
  79. package/dist/env-file.d.ts.map +1 -1
  80. package/dist/env-file.js +6 -0
  81. package/dist/env-file.js.map +1 -1
  82. package/dist/fix-recipes.d.ts +2 -0
  83. package/dist/fix-recipes.d.ts.map +1 -1
  84. package/dist/fix-recipes.js +102 -2
  85. package/dist/fix-recipes.js.map +1 -1
  86. package/dist/hand-built-manifest.d.ts +27 -0
  87. package/dist/hand-built-manifest.d.ts.map +1 -0
  88. package/dist/hand-built-manifest.js +124 -0
  89. package/dist/hand-built-manifest.js.map +1 -0
  90. package/dist/hooks-report.d.ts +23 -0
  91. package/dist/hooks-report.d.ts.map +1 -0
  92. package/dist/hooks-report.js +104 -0
  93. package/dist/hooks-report.js.map +1 -0
  94. package/dist/init-agents.d.ts +18 -2
  95. package/dist/init-agents.d.ts.map +1 -1
  96. package/dist/init-agents.js +20 -13
  97. package/dist/init-agents.js.map +1 -1
  98. package/dist/internal/config-paths.d.ts +16 -0
  99. package/dist/internal/config-paths.d.ts.map +1 -0
  100. package/dist/internal/config-paths.js +124 -0
  101. package/dist/internal/config-paths.js.map +1 -0
  102. package/dist/internal/installed-package.d.ts +4 -0
  103. package/dist/internal/installed-package.d.ts.map +1 -0
  104. package/dist/internal/installed-package.js +17 -0
  105. package/dist/internal/installed-package.js.map +1 -0
  106. package/dist/internal/nifra-project.d.ts +41 -0
  107. package/dist/internal/nifra-project.d.ts.map +1 -0
  108. package/dist/internal/nifra-project.js +183 -0
  109. package/dist/internal/nifra-project.js.map +1 -0
  110. package/dist/internal/project-specifiers.d.ts +29 -0
  111. package/dist/internal/project-specifiers.d.ts.map +1 -0
  112. package/dist/internal/project-specifiers.js +435 -0
  113. package/dist/internal/project-specifiers.js.map +1 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -1
  115. package/dist/internal/script-kind.js +2 -1
  116. package/dist/internal/script-kind.js.map +1 -1
  117. package/dist/internal/source-facts.d.ts +8 -1
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +22 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +1 -1
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +10 -6
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +15 -6
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +87 -25
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/levels-tool.d.ts.map +1 -1
  130. package/dist/levels-tool.js +98 -23
  131. package/dist/levels-tool.js.map +1 -1
  132. package/dist/load.d.ts +33 -10
  133. package/dist/load.d.ts.map +1 -1
  134. package/dist/load.js +60 -10
  135. package/dist/load.js.map +1 -1
  136. package/dist/mcp-context.d.ts +5 -5
  137. package/dist/mcp-context.d.ts.map +1 -1
  138. package/dist/mcp-context.js +8 -23
  139. package/dist/mcp-context.js.map +1 -1
  140. package/dist/mcp-delegate.d.ts +2 -2
  141. package/dist/mcp-delegate.d.ts.map +1 -1
  142. package/dist/mcp-delegate.js +19 -7
  143. package/dist/mcp-delegate.js.map +1 -1
  144. package/dist/mcp-exec.d.ts +13 -4
  145. package/dist/mcp-exec.d.ts.map +1 -1
  146. package/dist/mcp-exec.js +56 -14
  147. package/dist/mcp-exec.js.map +1 -1
  148. package/dist/mcp-http.js +1 -1
  149. package/dist/mcp-io.d.ts +30 -0
  150. package/dist/mcp-io.d.ts.map +1 -1
  151. package/dist/mcp-io.js +63 -0
  152. package/dist/mcp-io.js.map +1 -1
  153. package/dist/mcp-isolate.d.ts.map +1 -1
  154. package/dist/mcp-isolate.js +3 -2
  155. package/dist/mcp-isolate.js.map +1 -1
  156. package/dist/mcp-path.d.ts +2 -1
  157. package/dist/mcp-path.d.ts.map +1 -1
  158. package/dist/mcp-path.js +9 -24
  159. package/dist/mcp-path.js.map +1 -1
  160. package/dist/mcp-project-child.js +1 -1
  161. package/dist/mcp-project-child.js.map +1 -1
  162. package/dist/mcp-root.d.ts.map +1 -1
  163. package/dist/mcp-root.js +50 -10
  164. package/dist/mcp-root.js.map +1 -1
  165. package/dist/mcp-ws.d.ts.map +1 -1
  166. package/dist/mcp-ws.js +3 -1
  167. package/dist/mcp-ws.js.map +1 -1
  168. package/dist/mcp.d.ts.map +1 -1
  169. package/dist/mcp.js +48 -19
  170. package/dist/mcp.js.map +1 -1
  171. package/dist/migrate-layout.d.ts +47 -1
  172. package/dist/migrate-layout.d.ts.map +1 -1
  173. package/dist/migrate-layout.js +1972 -170
  174. package/dist/migrate-layout.js.map +1 -1
  175. package/dist/output-schemas.d.ts +128 -0
  176. package/dist/output-schemas.d.ts.map +1 -0
  177. package/dist/output-schemas.js +975 -0
  178. package/dist/output-schemas.js.map +1 -0
  179. package/dist/port.d.ts.map +1 -1
  180. package/dist/port.js +3 -2
  181. package/dist/port.js.map +1 -1
  182. package/dist/project-facts.d.ts +15 -4
  183. package/dist/project-facts.d.ts.map +1 -1
  184. package/dist/project-facts.js +1 -0
  185. package/dist/project-facts.js.map +1 -1
  186. package/dist/project-path.d.ts +6 -0
  187. package/dist/project-path.d.ts.map +1 -1
  188. package/dist/project-path.js +48 -14
  189. package/dist/project-path.js.map +1 -1
  190. package/dist/recipes/5.0.0.d.ts +11 -0
  191. package/dist/recipes/5.0.0.d.ts.map +1 -0
  192. package/dist/recipes/5.0.0.js +34 -0
  193. package/dist/recipes/5.0.0.js.map +1 -0
  194. package/dist/recipes/index.d.ts +7 -0
  195. package/dist/recipes/index.d.ts.map +1 -1
  196. package/dist/recipes/index.js +2 -0
  197. package/dist/recipes/index.js.map +1 -1
  198. package/dist/release-verification.d.ts +5 -0
  199. package/dist/release-verification.d.ts.map +1 -1
  200. package/dist/release-verification.js +14 -0
  201. package/dist/release-verification.js.map +1 -1
  202. package/dist/replay.d.ts.map +1 -1
  203. package/dist/replay.js +12 -3
  204. package/dist/replay.js.map +1 -1
  205. package/dist/review.d.ts +3 -1
  206. package/dist/review.d.ts.map +1 -1
  207. package/dist/review.js +43 -7
  208. package/dist/review.js.map +1 -1
  209. package/dist/rules/codes.d.ts +12 -0
  210. package/dist/rules/codes.d.ts.map +1 -1
  211. package/dist/rules/codes.js +12 -0
  212. package/dist/rules/codes.js.map +1 -1
  213. package/dist/rules/comment-markers.d.ts +16 -7
  214. package/dist/rules/comment-markers.d.ts.map +1 -1
  215. package/dist/rules/comment-markers.js +58 -19
  216. package/dist/rules/comment-markers.js.map +1 -1
  217. package/dist/rules/contract-visibility.d.ts +3 -0
  218. package/dist/rules/contract-visibility.d.ts.map +1 -0
  219. package/dist/rules/contract-visibility.js +337 -0
  220. package/dist/rules/contract-visibility.js.map +1 -0
  221. package/dist/rules/data-guard.d.ts +13 -0
  222. package/dist/rules/data-guard.d.ts.map +1 -1
  223. package/dist/rules/data-guard.js +356 -37
  224. package/dist/rules/data-guard.js.map +1 -1
  225. package/dist/rules/layout.d.ts +4 -0
  226. package/dist/rules/layout.d.ts.map +1 -0
  227. package/dist/rules/layout.js +19 -0
  228. package/dist/rules/layout.js.map +1 -0
  229. package/dist/rules/legacy.d.ts.map +1 -1
  230. package/dist/rules/legacy.js +178 -44
  231. package/dist/rules/legacy.js.map +1 -1
  232. package/dist/rules/pages.d.ts +50 -0
  233. package/dist/rules/pages.d.ts.map +1 -1
  234. package/dist/rules/pages.js +436 -4
  235. package/dist/rules/pages.js.map +1 -1
  236. package/dist/rules/routes.d.ts +6 -0
  237. package/dist/rules/routes.d.ts.map +1 -1
  238. package/dist/rules/routes.js +111 -16
  239. package/dist/rules/routes.js.map +1 -1
  240. package/dist/rules/secrets.js +2 -2
  241. package/dist/rules/secrets.js.map +1 -1
  242. package/dist/rules/security.d.ts +13 -1
  243. package/dist/rules/security.d.ts.map +1 -1
  244. package/dist/rules/security.js +313 -69
  245. package/dist/rules/security.js.map +1 -1
  246. package/dist/rules/zones.d.ts +13 -0
  247. package/dist/rules/zones.d.ts.map +1 -1
  248. package/dist/rules/zones.js +218 -44
  249. package/dist/rules/zones.js.map +1 -1
  250. package/dist/scaffold.d.ts.map +1 -1
  251. package/dist/scaffold.js +6 -4
  252. package/dist/scaffold.js.map +1 -1
  253. package/dist/sdk.d.ts.map +1 -1
  254. package/dist/sdk.js +14 -5
  255. package/dist/sdk.js.map +1 -1
  256. package/dist/stylex-migrate.d.ts.map +1 -1
  257. package/dist/stylex-migrate.js +290 -24
  258. package/dist/stylex-migrate.js.map +1 -1
  259. package/dist/test-tool.d.ts.map +1 -1
  260. package/dist/test-tool.js +6 -3
  261. package/dist/test-tool.js.map +1 -1
  262. package/dist/typecheck-coverage.d.ts +20 -0
  263. package/dist/typecheck-coverage.d.ts.map +1 -0
  264. package/dist/typecheck-coverage.js +129 -0
  265. package/dist/typecheck-coverage.js.map +1 -0
  266. package/dist/upgrade.d.ts +84 -0
  267. package/dist/upgrade.d.ts.map +1 -1
  268. package/dist/upgrade.js +498 -78
  269. package/dist/upgrade.js.map +1 -1
  270. package/dist/verification-plan.d.ts.map +1 -1
  271. package/dist/verification-plan.js +16 -2
  272. package/dist/verification-plan.js.map +1 -1
  273. package/dist/version-drift.d.ts +22 -0
  274. package/dist/version-drift.d.ts.map +1 -0
  275. package/dist/version-drift.js +94 -0
  276. package/dist/version-drift.js.map +1 -0
  277. package/dist/web-app-options.d.ts +1 -1
  278. package/dist/web-app-options.d.ts.map +1 -1
  279. package/dist/web-app-options.js +1 -1
  280. package/dist/web-app-options.js.map +1 -1
  281. package/dist/work-graph.d.ts.map +1 -1
  282. package/dist/work-graph.js +20 -8
  283. package/dist/work-graph.js.map +1 -1
  284. package/docs/examples.json +55 -7
  285. package/docs/llms-full.txt +2043 -222
  286. package/docs/types.json +1573 -246
  287. package/package.json +11 -12
  288. package/src/agent-files.ts +203 -0
  289. package/src/app-files.ts +34 -4
  290. package/src/assure-hydration.ts +18 -2
  291. package/src/assure.ts +88 -10
  292. package/src/capabilities-tool.ts +47 -9
  293. package/src/check-diagnostics.ts +75 -9
  294. package/src/check-scan.ts +1816 -377
  295. package/src/check.ts +258 -81
  296. package/src/cli.ts +242 -37
  297. package/src/client-module.ts +97 -0
  298. package/src/command-catalog.ts +237 -52
  299. package/src/component-check.ts +79 -0
  300. package/src/contract-proof.ts +3 -25
  301. package/src/contracts.ts +60 -3
  302. package/src/db-tool.ts +5 -2
  303. package/src/dev-bun-config.ts +51 -10
  304. package/src/dev-feed-tool.ts +112 -1
  305. package/src/diagnostics.ts +3 -0
  306. package/src/diff-tool.ts +33 -26
  307. package/src/doctor.ts +43 -21
  308. package/src/env-file.ts +7 -0
  309. package/src/fix-recipes.ts +116 -2
  310. package/src/hand-built-manifest.ts +144 -0
  311. package/src/hooks-report.ts +135 -0
  312. package/src/init-agents.ts +21 -14
  313. package/src/internal/config-paths.ts +154 -0
  314. package/src/internal/installed-package.ts +15 -0
  315. package/src/internal/nifra-project.ts +191 -0
  316. package/src/internal/project-specifiers.ts +477 -0
  317. package/src/internal/script-kind.ts +2 -1
  318. package/src/internal/source-facts.ts +48 -5
  319. package/src/internal/typescript-import.ts +15 -6
  320. package/src/introspect.ts +104 -28
  321. package/src/levels-tool.ts +124 -29
  322. package/src/load.ts +107 -17
  323. package/src/mcp-context.ts +12 -25
  324. package/src/mcp-delegate.ts +22 -10
  325. package/src/mcp-exec.ts +68 -18
  326. package/src/mcp-http.ts +1 -1
  327. package/src/mcp-io.ts +74 -0
  328. package/src/mcp-isolate.ts +9 -2
  329. package/src/mcp-path.ts +8 -22
  330. package/src/mcp-project-child.ts +1 -1
  331. package/src/mcp-root.ts +58 -8
  332. package/src/mcp-ws.ts +4 -2
  333. package/src/mcp.ts +56 -22
  334. package/src/migrate-layout.ts +2257 -176
  335. package/src/output-schemas.ts +1240 -0
  336. package/src/port.ts +3 -2
  337. package/src/project-facts.ts +17 -3
  338. package/src/project-path.ts +44 -13
  339. package/src/recipes/5.0.0.ts +35 -0
  340. package/src/recipes/index.ts +10 -0
  341. package/src/release-verification.ts +20 -0
  342. package/src/replay.ts +14 -3
  343. package/src/review.ts +44 -10
  344. package/src/rules/codes.ts +12 -0
  345. package/src/rules/comment-markers.ts +57 -21
  346. package/src/rules/contract-visibility.ts +383 -0
  347. package/src/rules/data-guard.ts +391 -42
  348. package/src/rules/layout.ts +22 -0
  349. package/src/rules/legacy.ts +206 -52
  350. package/src/rules/pages.ts +481 -4
  351. package/src/rules/routes.ts +124 -16
  352. package/src/rules/secrets.ts +2 -2
  353. package/src/rules/security.ts +391 -72
  354. package/src/rules/zones.ts +232 -42
  355. package/src/scaffold.ts +6 -4
  356. package/src/sdk.ts +14 -5
  357. package/src/stylex-migrate.ts +251 -24
  358. package/src/test-tool.ts +12 -3
  359. package/src/typecheck-coverage.ts +134 -0
  360. package/src/upgrade.ts +576 -72
  361. package/src/verification-plan.ts +33 -2
  362. package/src/version-drift.ts +118 -0
  363. package/src/web-app-options.ts +2 -2
  364. package/src/work-graph.ts +17 -12
@@ -0,0 +1,975 @@
1
+ /**
2
+ * Output schemas from a route's own return types. `loaderOutput` and `actionOutput` stay the reviewed
3
+ * allow-list of what reaches the browser: nothing here runs at request time, and no schema is derived
4
+ * implicitly. `nifra schemas` reads each loader's and action's TypeScript return type, writes a
5
+ * schema for review where a route has none, keeps the schemas it wrote current, and names the
6
+ * returned fields a hand-written schema drops. `nifra check` (NF-C033) fails on a generated schema
7
+ * whose route has changed what it returns since.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
12
+ import { basename, join } from "node:path";
13
+ import { isSensitiveFieldName } from "@nifrajs/web/zones";
14
+ import { codePositionMask } from "./check-scan.js";
15
+ import { codeUnitOrder } from "./internal/code-unit-order.js";
16
+ import { loadProjectTypeScript, } from "./internal/typescript-import.js";
17
+ import { exported, expressionEnd } from "./rules/data-guard.js";
18
+ const PAIRS = [
19
+ ["loader", "loaderOutput"],
20
+ ["action", "actionOutput"],
21
+ ];
22
+ /** The comment that marks a schema `nifra schemas` wrote, with the hash of what it wrote. */
23
+ export const GENERATED_MARK = "nifra:generated";
24
+ const MARKER = /^[ \t]*\/\/ nifra:generated ([0-9a-f]{8})\b/;
25
+ const FLAG_NAMES = [
26
+ "Any",
27
+ "Unknown",
28
+ "Never",
29
+ "Undefined",
30
+ "Void",
31
+ "Null",
32
+ "StringLiteral",
33
+ "NumberLiteral",
34
+ "BooleanLiteral",
35
+ "StringLike",
36
+ "NumberLike",
37
+ "BooleanLike",
38
+ "BigIntLike",
39
+ "ESSymbolLike",
40
+ "Union",
41
+ "Intersection",
42
+ "Object",
43
+ "NonPrimitive",
44
+ ];
45
+ const flagsFrom = (read) => {
46
+ const at = (name) => {
47
+ const value = read(name);
48
+ return typeof value === "number" ? value : 0;
49
+ };
50
+ return {
51
+ Any: at("Any"),
52
+ Unknown: at("Unknown"),
53
+ Never: at("Never"),
54
+ Undefined: at("Undefined"),
55
+ Void: at("Void"),
56
+ Null: at("Null"),
57
+ StringLiteral: at("StringLiteral"),
58
+ NumberLiteral: at("NumberLiteral"),
59
+ BooleanLiteral: at("BooleanLiteral"),
60
+ StringLike: at("StringLike"),
61
+ NumberLike: at("NumberLike"),
62
+ BooleanLike: at("BooleanLike"),
63
+ BigIntLike: at("BigIntLike"),
64
+ ESSymbolLike: at("ESSymbolLike"),
65
+ Union: at("Union"),
66
+ Intersection: at("Intersection"),
67
+ Object: at("Object"),
68
+ NonPrimitive: at("NonPrimitive"),
69
+ };
70
+ };
71
+ const MAX_DEPTH = 12;
72
+ const JSON_ALIASES = new Set(["JsonValue", "JSONValue", "Json"]);
73
+ const NOT_JSON = new Set([
74
+ "Map",
75
+ "Set",
76
+ "WeakMap",
77
+ "WeakSet",
78
+ "RegExp",
79
+ "Error",
80
+ "Promise",
81
+ "URL",
82
+ "URLSearchParams",
83
+ "Headers",
84
+ "Request",
85
+ "Response",
86
+ "Blob",
87
+ "File",
88
+ "FormData",
89
+ "ArrayBuffer",
90
+ "SharedArrayBuffer",
91
+ "DataView",
92
+ "ReadableStream",
93
+ "WritableStream",
94
+ "Function",
95
+ ]);
96
+ const TYPED_ARRAY = /^(?:Big)?(?:Int|Uint|Float)(?:8|16|32|64)(?:Clamped)?Array$/;
97
+ const sameShape = (a, b) => JSON.stringify(a) === JSON.stringify(b);
98
+ function unionOf(input) {
99
+ const flat = input
100
+ .flatMap((member) => (member.kind === "union" ? member.members : [member]))
101
+ .filter((member) => member.kind !== "never");
102
+ const json = flat.find((member) => member.kind === "json");
103
+ if (json !== undefined)
104
+ return json;
105
+ const isBool = (member) => member.kind === "literal" && typeof member.value === "boolean";
106
+ const both = flat.some((m) => m.kind === "literal" && m.value === true) &&
107
+ flat.some((m) => m.kind === "literal" && m.value === false);
108
+ const members = [];
109
+ for (const member of flat) {
110
+ const next = both && isBool(member) ? { kind: "boolean" } : member;
111
+ if (!members.some((seen) => sameShape(seen, next)))
112
+ members.push(next);
113
+ }
114
+ if (members.length === 0)
115
+ return { kind: "never" };
116
+ if (members.length === 1 && members[0] !== undefined)
117
+ return members[0];
118
+ // TypeScript versions order union members differently; a canonical order keeps the generated text,
119
+ // and the hash in its marker, the same under each. `null` reads best last.
120
+ const rank = (shape) => `${shape.kind === "null" || shape.kind === "undefined" ? 1 : 0}${JSON.stringify(shape)}`;
121
+ return { kind: "union", members: members.sort((a, b) => codeUnitOrder(rank(a), rank(b))) };
122
+ }
123
+ function withoutUndefined(shape) {
124
+ if (shape.kind !== "union" || !shape.members.some((member) => member.kind === "undefined"))
125
+ return shape;
126
+ return unionOf(shape.members.filter((member) => member.kind !== "undefined"));
127
+ }
128
+ async function shapeOf(reader, type, seen, depth) {
129
+ if (depth > MAX_DEPTH)
130
+ return { kind: "json", reason: "nested too deep to read" };
131
+ const f = reader.flag;
132
+ const flags = reader.flags(type);
133
+ const is = (mask) => (flags & mask) !== 0;
134
+ if (is(f.Any))
135
+ return { kind: "json", reason: "any" };
136
+ if (is(f.Unknown))
137
+ return { kind: "json", reason: "unknown" };
138
+ if (is(f.Never))
139
+ return { kind: "never" };
140
+ if (is(f.Undefined | f.Void))
141
+ return { kind: "undefined" };
142
+ if (is(f.Null))
143
+ return { kind: "null" };
144
+ if (is(f.StringLiteral | f.NumberLiteral | f.BooleanLiteral)) {
145
+ const value = await reader.literal(type);
146
+ if (value !== undefined)
147
+ return { kind: "literal", value };
148
+ }
149
+ if (is(f.StringLike))
150
+ return { kind: "string" };
151
+ if (is(f.NumberLike))
152
+ return { kind: "number" };
153
+ // `boolean` is the union `true | false` with the Boolean flag, so this precedes the union branch.
154
+ if (is(f.BooleanLike))
155
+ return { kind: "boolean" };
156
+ if (is(f.BigIntLike))
157
+ return { kind: "opaque", what: "a bigint" };
158
+ if (is(f.ESSymbolLike))
159
+ return { kind: "opaque", what: "a symbol" };
160
+ const alias = await reader.aliasName(type);
161
+ if (alias !== undefined && JSON_ALIASES.has(alias))
162
+ return { kind: "json" };
163
+ if (is(f.Union)) {
164
+ const members = await reader.members(type);
165
+ return unionOf(await Promise.all(members.map((member) => shapeOf(reader, member, seen, depth + 1))));
166
+ }
167
+ if (seen.has(type))
168
+ return { kind: "json", reason: "a recursive type" };
169
+ if (is(f.Intersection)) {
170
+ // A branded primitive (`string & { __brand: "Id" }`) crosses the wire as the primitive.
171
+ for (const part of await reader.members(type)) {
172
+ if ((reader.flags(part) & (f.StringLike | f.NumberLike | f.BooleanLike)) !== 0)
173
+ return shapeOf(reader, part, seen, depth + 1);
174
+ }
175
+ }
176
+ if (!is(f.Object | f.Intersection | f.NonPrimitive))
177
+ return { kind: "opaque", what: await reader.text(type) };
178
+ const tuple = await reader.tupleElements(type);
179
+ if (tuple !== undefined) {
180
+ return {
181
+ kind: "tuple",
182
+ items: await Promise.all(tuple.map((item) => shapeOf(reader, item, seen, depth + 1))),
183
+ };
184
+ }
185
+ const element = await reader.arrayElement(type);
186
+ if (element !== undefined)
187
+ return { kind: "array", items: await shapeOf(reader, element, seen, depth + 1) };
188
+ const name = await reader.symbolName(type);
189
+ if (name === "Date")
190
+ return { kind: "date" };
191
+ if (name !== undefined && (NOT_JSON.has(name) || TYPED_ARRAY.test(name)))
192
+ return { kind: "opaque", what: `a ${name}` };
193
+ if (await reader.callable(type))
194
+ return { kind: "opaque", what: "a function" };
195
+ seen.add(type);
196
+ try {
197
+ const properties = await reader.properties(type);
198
+ // A `Response`, or nifra's own response carrier (`redirect()`, `status()`): control flow, not data.
199
+ if (properties.some((property) => property.name === "toResponse"))
200
+ return { kind: "opaque", what: "a Response" };
201
+ const promise = properties.find((property) => property.name === "promise");
202
+ if (promise !== undefined && properties.some((p) => p.name === "__nifra_deferred")) {
203
+ const [inner] = await reader.typeArguments(promise.type);
204
+ return {
205
+ kind: "deferred",
206
+ value: inner === undefined
207
+ ? { kind: "json", reason: "unknown" }
208
+ : await shapeOf(reader, inner, seen, depth + 1),
209
+ };
210
+ }
211
+ const index = await reader.stringIndex(type);
212
+ if (index !== undefined && properties.length === 0)
213
+ return { kind: "record", values: await shapeOf(reader, index, seen, depth + 1) };
214
+ const fields = [];
215
+ for (const property of properties) {
216
+ if (property.name.startsWith("__@") || (await reader.callable(property.type)))
217
+ continue;
218
+ const shape = await shapeOf(reader, property.type, seen, depth + 1);
219
+ // `key?: undefined` is how TypeScript lines up a union of object literals: a key the other
220
+ // members have and this one never sends.
221
+ if (shape.kind === "undefined" || shape.kind === "never")
222
+ continue;
223
+ const defined = withoutUndefined(shape);
224
+ fields.push({
225
+ name: property.name,
226
+ optional: property.optional || defined !== shape,
227
+ shape: defined,
228
+ });
229
+ }
230
+ return { kind: "object", fields };
231
+ }
232
+ finally {
233
+ seen.delete(type);
234
+ }
235
+ }
236
+ /** What a loader's return type sends: control-flow returns (nothing, a `Response`) carry no data. */
237
+ function dataShape(shape) {
238
+ const members = shape.kind === "union" ? shape.members : [shape];
239
+ return unionOf(members.filter((member) => member.kind !== "undefined" && !(member.kind === "opaque" && member.what === "a Response")));
240
+ }
241
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
242
+ const note = (text) => text.replaceAll("*/", "* /");
243
+ /** `shape` as `t.*` source, objects one field per line below `indent`. */
244
+ export function schemaSource(shape, indent = "") {
245
+ switch (shape.kind) {
246
+ case "string":
247
+ case "number":
248
+ case "boolean":
249
+ case "null":
250
+ case "date":
251
+ return `t.${shape.kind}()`;
252
+ case "undefined":
253
+ case "never":
254
+ return "t.null()";
255
+ case "json":
256
+ return shape.reason === undefined
257
+ ? "t.json()"
258
+ : `t.json() /* ${note(shape.reason)} in the source type */`;
259
+ case "opaque":
260
+ return `t.json() /* ${note(shape.what)}, which is not JSON */`;
261
+ case "literal":
262
+ return `t.literal(${JSON.stringify(shape.value)})`;
263
+ case "array":
264
+ return `t.array(${schemaSource(shape.items, indent)})`;
265
+ case "tuple":
266
+ return `t.array(${schemaSource(unionOf(shape.items), indent)})`;
267
+ case "record":
268
+ return `t.record(${schemaSource(shape.values, indent)})`;
269
+ case "deferred":
270
+ return `t.deferred(${schemaSource(shape.value, indent)})`;
271
+ case "union":
272
+ return `t.union([${shape.members.map((member) => schemaSource(member, indent)).join(", ")}])`;
273
+ case "object": {
274
+ if (shape.fields.length === 0)
275
+ return "t.object({})";
276
+ const inner = `${indent} `;
277
+ const lines = shape.fields.map((field) => {
278
+ const key = IDENTIFIER.test(field.name) ? field.name : JSON.stringify(field.name);
279
+ if (isSensitiveFieldName(field.name))
280
+ return `${inner}// left out: ${key} - a credential-like name; declare it with t.declassified("why", ...) if it must reach the browser`;
281
+ if (field.shape.kind === "opaque")
282
+ return `${inner}// left out: ${key} - ${note(field.shape.what)}, which is not JSON`;
283
+ const value = schemaSource(field.shape, inner);
284
+ return `${inner}${key}: ${field.optional ? `t.optional(${value})` : value},`;
285
+ });
286
+ return `t.object({\n${lines.join("\n")}\n${indent}})`;
287
+ }
288
+ }
289
+ }
290
+ /** The paths a draft of `shape` leaves out: credential-like names, and values that are not JSON. */
291
+ export function omittedPaths(shape, path = "") {
292
+ switch (shape.kind) {
293
+ case "array":
294
+ return omittedPaths(shape.items, `${path}[]`);
295
+ case "tuple":
296
+ return omittedPaths(unionOf(shape.items), `${path}[]`);
297
+ case "record":
298
+ return omittedPaths(shape.values, `${path}{}`);
299
+ case "deferred":
300
+ return omittedPaths(shape.value, path);
301
+ case "union":
302
+ return [...new Set(shape.members.flatMap((member) => omittedPaths(member, path)))];
303
+ case "object":
304
+ return shape.fields.flatMap((field) => {
305
+ const at = path === "" ? field.name : `${path}.${field.name}`;
306
+ return isSensitiveFieldName(field.name) || field.shape.kind === "opaque"
307
+ ? [at]
308
+ : omittedPaths(field.shape, at);
309
+ });
310
+ default:
311
+ return [];
312
+ }
313
+ }
314
+ /** The paths a draft of `shape` admits any value at: `any` or `unknown` there, or a type too deep or
315
+ * recursive to read. The output guard drops nothing below them. */
316
+ export function openPaths(shape, path = "") {
317
+ switch (shape.kind) {
318
+ case "json":
319
+ return shape.reason === undefined ? [] : [path === "" ? "(the whole value)" : path];
320
+ case "array":
321
+ return openPaths(shape.items, `${path}[]`);
322
+ case "tuple":
323
+ return openPaths(unionOf(shape.items), `${path}[]`);
324
+ case "record":
325
+ return openPaths(shape.values, `${path}{}`);
326
+ case "deferred":
327
+ return openPaths(shape.value, path);
328
+ case "union":
329
+ return [...new Set(shape.members.flatMap((member) => openPaths(member, path)))];
330
+ case "object":
331
+ return shape.fields.flatMap((field) => isSensitiveFieldName(field.name) || field.shape.kind === "opaque"
332
+ ? []
333
+ : openPaths(field.shape, path === "" ? field.name : `${path}.${field.name}`));
334
+ default:
335
+ return [];
336
+ }
337
+ }
338
+ /**
339
+ * The paths `returned` carries that `declared` does not, as `a.b`, `items[].c`: what the output guard
340
+ * drops before the page renders. A declared `t.json()` or record admits everything below it.
341
+ */
342
+ export function undeclaredPaths(returned, declared, path = "") {
343
+ if (declared.kind === "json" || declared.kind === "record")
344
+ return [];
345
+ if (returned.kind === "union") {
346
+ const found = returned.members.flatMap((member) => undeclaredPaths(member, declared, path));
347
+ return [...new Set(found)];
348
+ }
349
+ if (declared.kind === "union") {
350
+ const candidates = declared.members.filter((m) => m.kind !== "null" && m.kind !== "undefined");
351
+ if (candidates.some((m) => m.kind === "json" || m.kind === "record"))
352
+ return [];
353
+ const results = candidates.map((candidate) => undeclaredPaths(returned, candidate, path));
354
+ const [first, ...rest] = results;
355
+ return first === undefined ? [] : first.filter((p) => rest.every((r) => r.includes(p)));
356
+ }
357
+ if (returned.kind === "deferred")
358
+ return undeclaredPaths(returned.value, declared.kind === "deferred" ? declared.value : declared, path);
359
+ if (declared.kind === "deferred")
360
+ return undeclaredPaths(returned, declared.value, path);
361
+ const items = (shape) => shape.kind === "array" ? shape.items : shape.kind === "tuple" ? unionOf(shape.items) : undefined;
362
+ const returnedItems = items(returned);
363
+ const declaredItems = items(declared);
364
+ if (returnedItems !== undefined && declaredItems !== undefined)
365
+ return undeclaredPaths(returnedItems, declaredItems, `${path}[]`);
366
+ if (returned.kind !== "object" || declared.kind !== "object")
367
+ return [];
368
+ const found = [];
369
+ for (const field of returned.fields) {
370
+ const at = path === "" ? field.name : `${path}.${field.name}`;
371
+ const match = declared.fields.find((candidate) => candidate.name === field.name);
372
+ if (match === undefined)
373
+ found.push(at);
374
+ else
375
+ found.push(...undeclaredPaths(field.shape, match.shape, at));
376
+ }
377
+ return found;
378
+ }
379
+ const hashOf = (body) => createHash("sha256").update(body).digest("hex").slice(0, 8);
380
+ /**
381
+ * A schema expression as a formatter cannot change it: comments, whitespace, a comma before a closing
382
+ * bracket and a trailing semicolon dropped, every string written with double quotes, and a quoted key
383
+ * that needs no quotes written bare. Two texts
384
+ * that read the same here are the same schema; an edit to what it declares is not.
385
+ */
386
+ function schemaTokens(text) {
387
+ let out = "";
388
+ let i = 0;
389
+ while (i < text.length) {
390
+ const c = text.charAt(i);
391
+ if (c === "/" && text[i + 1] === "/") {
392
+ const end = text.indexOf("\n", i);
393
+ i = end === -1 ? text.length : end;
394
+ }
395
+ else if (c === "/" && text[i + 1] === "*") {
396
+ const end = text.indexOf("*/", i + 2);
397
+ i = end === -1 ? text.length : end + 2;
398
+ }
399
+ else if (c === '"' || c === "'" || c === "`") {
400
+ let value = "";
401
+ let j = i + 1;
402
+ while (j < text.length && text[j] !== c) {
403
+ if (text[j] === "\\") {
404
+ const next = text[j + 1] ?? "";
405
+ value += next === c ? next : `\\${next}`;
406
+ j += 2;
407
+ }
408
+ else {
409
+ value += text[j];
410
+ j++;
411
+ }
412
+ }
413
+ i = j + 1;
414
+ let after = i;
415
+ while (/\s/.test(text[after] ?? ""))
416
+ after++;
417
+ // `"name": …` and `name: …` declare the same key.
418
+ out +=
419
+ text[after] === ":" && IDENTIFIER.test(value) ? value : `"${value.replaceAll('"', '\\"')}"`;
420
+ }
421
+ else {
422
+ if (!/\s/.test(c))
423
+ out += c;
424
+ i++;
425
+ }
426
+ }
427
+ return out.replace(/,(?=[}\])])/g, "").replace(/;+$/, "");
428
+ }
429
+ function declarationText(schema, fn, body) {
430
+ return [
431
+ `// ${GENERATED_MARK} ${hashOf(body)} - drafted from the ${fn}'s return type; review that the browser may see every field it declares. \`nifra schemas --write\` keeps it current; delete this line to edit it by hand.`,
432
+ `export const ${schema} = ${body}`,
433
+ ].join("\n");
434
+ }
435
+ const posix = (path) => path.replaceAll("\\", "/");
436
+ const PROBE_HELPERS = [
437
+ "type __NifraReturned<M, F extends string> = M extends { readonly [K in F]: (...args: any) => infer R } ? Awaited<R> : never",
438
+ 'type __NifraDeclared<M, N extends string> = M extends { readonly [K in N]: infer S } ? S extends { readonly "~standard": { readonly types?: infer T } } ? NonNullable<T> extends { readonly output: infer O } ? O : never : never : never',
439
+ ];
440
+ function probeSource(routes) {
441
+ const lines = [...PROBE_HELPERS];
442
+ const names = new Map();
443
+ routes.forEach((route, i) => {
444
+ const rel = posix(route.file);
445
+ lines.push(`import type * as __nifra_m${i} from ${JSON.stringify(rel.startsWith(".") ? rel : `./${rel}`)}`);
446
+ for (const fn of route.fns) {
447
+ const name = `__nifra_r${i}_${fn}`;
448
+ names.set(name, `${route.file}#${fn}`);
449
+ lines.push(`declare const ${name}: __NifraReturned<typeof __nifra_m${i}, "${fn}">`);
450
+ }
451
+ for (const schema of route.schemas) {
452
+ const name = `__nifra_d${i}_${schema}`;
453
+ names.set(name, `${route.file}#${schema}`);
454
+ lines.push(`declare const ${name}: __NifraDeclared<typeof __nifra_m${i}, "${schema}">`);
455
+ }
456
+ });
457
+ return { text: `${lines.join("\n")}\n`, names };
458
+ }
459
+ function legacyReader(ts, checker, location) {
460
+ const isReference = (type) => "target" in type;
461
+ return {
462
+ flag: flagsFrom((name) => Reflect.get(ts.TypeFlags, name)),
463
+ flags: (type) => type.flags,
464
+ members: async (type) => (type.isUnionOrIntersection() ? type.types : []),
465
+ symbolName: async (type) => type.getSymbol()?.name,
466
+ aliasName: async (type) => type.aliasSymbol?.name,
467
+ literal: async (type) => {
468
+ if (type.isStringLiteral() || type.isNumberLiteral())
469
+ return type.value;
470
+ if ((type.flags & ts.TypeFlags.BooleanLiteral) !== 0)
471
+ return checker.typeToString(type) === "true";
472
+ return undefined;
473
+ },
474
+ arrayElement: async (type) => checker.isArrayType(type) && isReference(type)
475
+ ? checker.getTypeArguments(type)[0]
476
+ : undefined,
477
+ tupleElements: async (type) => checker.isTupleType(type) && isReference(type) ? checker.getTypeArguments(type) : undefined,
478
+ typeArguments: async (type) => (isReference(type) ? checker.getTypeArguments(type) : []),
479
+ properties: async (type) => checker.getPropertiesOfType(type).map((property) => ({
480
+ name: property.name,
481
+ optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
482
+ type: checker.getTypeOfSymbolAtLocation(property, location),
483
+ })),
484
+ stringIndex: async (type) => checker
485
+ .getIndexInfosOfType(type)
486
+ .find((info) => (info.keyType.flags & ts.TypeFlags.StringLike) !== 0)?.type,
487
+ callable: async (type) => checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0,
488
+ text: async (type) => checker.typeToString(type),
489
+ };
490
+ }
491
+ const CHECKER7_METHODS = [
492
+ "getTypeAtPosition",
493
+ "getTypeOfSymbol",
494
+ "getPropertiesOfType",
495
+ "getIndexInfosOfType",
496
+ "getSignaturesOfType",
497
+ "getTypeArguments",
498
+ "typeToString",
499
+ "isTupleType",
500
+ "isArrayType",
501
+ ];
502
+ function isChecker7(value) {
503
+ return (typeof value === "object" &&
504
+ value !== null &&
505
+ CHECKER7_METHODS.every((method) => typeof Reflect.get(value, method) === "function"));
506
+ }
507
+ function reader7(semantic, checker) {
508
+ const flag = flagsFrom((name) => semantic.typeFlags[name]);
509
+ const optional = semantic.symbolFlags.Optional;
510
+ const call = semantic.signatureKind.Call;
511
+ return {
512
+ flag,
513
+ flags: (type) => type.flags,
514
+ members: async (type) => (await type.getTypes?.()) ?? [],
515
+ symbolName: async (type) => (await type.getSymbol())?.name,
516
+ aliasName: async (type) => (await type.getAliasSymbol?.())?.name,
517
+ literal: async (type) => {
518
+ if ((type.flags & flag.BooleanLiteral) !== 0)
519
+ return (await checker.typeToString(type)) === "true";
520
+ return typeof type.value === "string" || typeof type.value === "number"
521
+ ? type.value
522
+ : undefined;
523
+ },
524
+ arrayElement: async (type) => (await checker.isArrayType(type)) ? (await checker.getTypeArguments(type))[0] : undefined,
525
+ tupleElements: async (type) => (await checker.isTupleType(type)) ? checker.getTypeArguments(type) : undefined,
526
+ typeArguments: async (type) => {
527
+ try {
528
+ return await checker.getTypeArguments(type);
529
+ }
530
+ catch {
531
+ return [];
532
+ }
533
+ },
534
+ properties: async (type) => {
535
+ const out = [];
536
+ for (const property of await checker.getPropertiesOfType(type)) {
537
+ const propertyType = await checker.getTypeOfSymbol(property);
538
+ if (propertyType === undefined)
539
+ continue;
540
+ out.push({
541
+ name: property.name,
542
+ optional: typeof optional === "number" && (property.flags & optional) !== 0,
543
+ type: propertyType,
544
+ });
545
+ }
546
+ return out;
547
+ },
548
+ stringIndex: async (type) => (await checker.getIndexInfosOfType(type)).find((info) => (info.keyType.flags & flag.StringLike) !== 0)?.valueType,
549
+ callable: async (type) => typeof call === "number" && (await checker.getSignaturesOfType(type, call)).length > 0,
550
+ text: (type) => checker.typeToString(type),
551
+ };
552
+ }
553
+ /** Read every listed route function's return type and schema's output type through one program. */
554
+ async function readShapes(root, routes, sources) {
555
+ const warnings = [];
556
+ const shapes = new Map();
557
+ if (routes.length === 0)
558
+ return { shapes, warnings };
559
+ // TypeScript 7 resolves node handles only inside a configured project's root, so the probe and a
560
+ // short-lived config extending the app's sit at the root, and are removed afterwards.
561
+ const temp = await mkdtemp(join(root, ".nifra-schemas-"));
562
+ const tempName = basename(temp);
563
+ const probePath = join(root, `.${tempName}.ts`);
564
+ const probeConfigPath = join(root, `.${tempName}.tsconfig.json`);
565
+ const { text: probe, names } = probeSource(routes);
566
+ const routeFiles = routes.map((route) => join(root, route.file));
567
+ let close;
568
+ try {
569
+ await writeFile(probePath, probe, "utf8");
570
+ const include = [...routes.map((route) => posix(route.file)), basename(probePath)];
571
+ await writeFile(probeConfigPath, JSON.stringify(existsSync(join(root, "tsconfig.json"))
572
+ ? {
573
+ extends: "./tsconfig.json",
574
+ compilerOptions: { allowImportingTsExtensions: true, noEmit: true },
575
+ include,
576
+ }
577
+ : {
578
+ compilerOptions: {
579
+ module: "ESNext",
580
+ moduleResolution: "Bundler",
581
+ target: "ESNext",
582
+ strict: true,
583
+ allowImportingTsExtensions: true,
584
+ noEmit: true,
585
+ skipLibCheck: true,
586
+ },
587
+ include,
588
+ }), "utf8");
589
+ // The routes and every planned file the caller supplies are read from `sources`, not the disk:
590
+ // a migration plans against the layout it has not written yet.
591
+ const contents = new Map([[probePath, probe]]);
592
+ for (const [file, text] of sources)
593
+ contents.set(join(root, file), text);
594
+ for (const file of routeFiles)
595
+ if (!contents.has(file))
596
+ contents.set(file, readFileSync(file, "utf8"));
597
+ const loaded = await loadProjectTypeScript(root, undefined, {
598
+ files: [...contents.keys()],
599
+ read: (file) => contents.get(file),
600
+ projectConfig: probeConfigPath,
601
+ });
602
+ close = loaded.session?.close;
603
+ if (loaded.unsupported !== undefined) {
604
+ warnings.push(`TypeScript ${loaded.unsupported.version} is outside the supported API`);
605
+ return { shapes, warnings };
606
+ }
607
+ const ts = loaded.compiler;
608
+ if (ts === undefined) {
609
+ warnings.push("TypeScript is not installed, so return types could not be read");
610
+ return { shapes, warnings };
611
+ }
612
+ const semantic = loaded.session?.semantic;
613
+ if (semantic !== undefined) {
614
+ const checker = (await semantic.getCheckerForFile(probePath)) ?? semantic.checker;
615
+ if (!isChecker7(checker)) {
616
+ warnings.push("the TypeScript 7 checker lacks the calls this needs");
617
+ return { shapes, warnings };
618
+ }
619
+ const reader = reader7(semantic, checker);
620
+ for (const [name, key] of names) {
621
+ const at = probe.indexOf(`declare const ${name}:`) + "declare const ".length;
622
+ const type = await checker.getTypeAtPosition(probePath, at);
623
+ if (type !== undefined)
624
+ shapes.set(key, await shapeOf(reader, type, new Set(), 0));
625
+ }
626
+ return { shapes, warnings };
627
+ }
628
+ const parsed = ts.getParsedCommandLineOfConfigFile(probeConfigPath, {}, {
629
+ ...ts.sys,
630
+ onUnRecoverableConfigFileDiagnostic: () => { },
631
+ });
632
+ const options = {
633
+ ...(parsed?.options ?? {}),
634
+ noEmit: true,
635
+ skipLibCheck: true,
636
+ allowImportingTsExtensions: true,
637
+ };
638
+ const program = ts.createProgram([probePath], options, overlaidHost(ts, options, contents));
639
+ const source = program.getSourceFile(probePath);
640
+ if (source === undefined) {
641
+ warnings.push("could not create the type probe");
642
+ return { shapes, warnings };
643
+ }
644
+ const checker = program.getTypeChecker();
645
+ const reader = legacyReader(ts, checker, source);
646
+ for (const statement of source.statements) {
647
+ if (!ts.isVariableStatement(statement))
648
+ continue;
649
+ for (const declaration of statement.declarationList.declarations) {
650
+ if (!ts.isIdentifier(declaration.name))
651
+ continue;
652
+ const key = names.get(declaration.name.text);
653
+ if (key === undefined)
654
+ continue;
655
+ const type = checker.getTypeAtLocation(declaration.name);
656
+ shapes.set(key, await shapeOf(reader, type, new Set(), 0));
657
+ }
658
+ }
659
+ return { shapes, warnings };
660
+ }
661
+ finally {
662
+ await close?.();
663
+ await rm(probePath, { force: true });
664
+ await rm(probeConfigPath, { force: true });
665
+ await rm(temp, { recursive: true, force: true });
666
+ }
667
+ }
668
+ /** A compiler host that reads `contents` (absolute path -> text) before the disk. */
669
+ function overlaidHost(ts, options, contents) {
670
+ const host = ts.createCompilerHost(options, true);
671
+ const key = (file) => posix(ts.sys.resolvePath(file));
672
+ const overlay = new Map([...contents].map(([file, text]) => [key(file), text]));
673
+ const folders = new Set();
674
+ for (const file of overlay.keys())
675
+ for (let at = file.lastIndexOf("/"); at > 0; at = file.lastIndexOf("/", at - 1))
676
+ folders.add(file.slice(0, at));
677
+ const { fileExists, readFile, getSourceFile, directoryExists } = host;
678
+ return Object.assign(host, {
679
+ fileExists: (file) => overlay.has(key(file)) || fileExists.call(host, file),
680
+ readFile: (file) => overlay.get(key(file)) ?? readFile.call(host, file),
681
+ directoryExists: (folder) => folders.has(key(folder)) ||
682
+ (directoryExists === undefined
683
+ ? ts.sys.directoryExists(folder)
684
+ : directoryExists.call(host, folder)),
685
+ getSourceFile: (file, language, onError, fresh) => {
686
+ const text = overlay.get(key(file));
687
+ return text === undefined
688
+ ? getSourceFile.call(host, file, language, onError, fresh)
689
+ : ts.createSourceFile(file, text, language);
690
+ },
691
+ });
692
+ }
693
+ const lineAt = (text, index) => text.slice(0, index).split("\n").length;
694
+ const lineStart = (text, index) => text.lastIndexOf("\n", index - 1) + 1;
695
+ /** The start of the comment block directly above the line at `start`, or `start`. */
696
+ function aboveComments(text, start) {
697
+ let at = start;
698
+ while (at > 0) {
699
+ const previous = lineStart(text, at - 1);
700
+ const line = text.slice(previous, at - 1).trim();
701
+ if (!(line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")))
702
+ break;
703
+ at = previous;
704
+ }
705
+ return at;
706
+ }
707
+ function declarationOf(content, mask, schema) {
708
+ const found = new RegExp(`\\bexport\\s+(?:const|let|var)\\s+${schema}\\b[^=]*=`).exec(mask);
709
+ if (found === null)
710
+ return exported(mask, schema) === null ? undefined : null;
711
+ const valueStart = found.index + found[0].length;
712
+ let end = expressionEnd(mask, valueStart);
713
+ if (content[end] === ";")
714
+ end++;
715
+ const exportStart = lineStart(content, found.index);
716
+ const markerStart = exportStart === 0 ? -1 : lineStart(content, exportStart - 1);
717
+ const marker = markerStart === -1 ? null : MARKER.exec(content.slice(markerStart, exportStart - 1));
718
+ return {
719
+ start: marker === null ? found.index : markerStart,
720
+ end,
721
+ value: content.slice(valueStart, end),
722
+ line: lineAt(content, found.index),
723
+ hash: marker?.[1],
724
+ };
725
+ }
726
+ function targetsOf(content) {
727
+ const mask = codePositionMask(content);
728
+ const targets = [];
729
+ for (const [fn, schema] of PAIRS) {
730
+ const found = exported(mask, fn);
731
+ if (found === null)
732
+ continue;
733
+ const reexport = found[0].startsWith("export {") || found[0].startsWith("export{");
734
+ let after = found.index + found[0].length;
735
+ while (/\s/.test(mask[after] ?? ""))
736
+ after++;
737
+ targets.push({
738
+ fn,
739
+ schema,
740
+ insertAt: reexport ? content.length : aboveComments(content, lineStart(content, found.index)),
741
+ fnLine: lineAt(content, found.index),
742
+ annotated: !reexport &&
743
+ /^(?:const|let|var)\b/.test(found[0].replace(/^export\s+/, "")) &&
744
+ mask[after] === ":",
745
+ declaration: declarationOf(content, mask, schema),
746
+ });
747
+ }
748
+ return targets;
749
+ }
750
+ const SCHEMA_IMPORT = /\bimport\s*(type\s*)?\{([^}]*)\}\s*from\s*["']@nifrajs\/schema["']/g;
751
+ const IMPORT = /^import\b[\s\S]*?from\s*["'][^"'\n]+["'];?|^import\s*["'][^"'\n]+["'];?/gm;
752
+ const T_BINDING = /\b(?:const|let|var|function|class)\s+t\b|\bimport\s+t\b|[{,]\s*t\s*[,}]|\bas\s+t\b/;
753
+ /**
754
+ * `content` with `t` imported from `@nifrajs/schema`, or `undefined` when `t` already names something
755
+ * else in the file. Strings are blank in the code mask, so imports are read from the text and kept
756
+ * only where the mask shows code.
757
+ */
758
+ function withSchemaImport(content) {
759
+ const mask = codePositionMask(content);
760
+ const inCode = (match) => mask[match.index] === "i";
761
+ const imports = [...content.matchAll(SCHEMA_IMPORT)].filter(inCode);
762
+ if (imports.some((m) => m[1] === undefined && /(?:^|,)\s*t\s*(?:,|$)/.test(m[2] ?? "")))
763
+ return content;
764
+ if (T_BINDING.test(mask))
765
+ return undefined;
766
+ const named = imports.find((m) => m[1] === undefined);
767
+ if (named !== undefined) {
768
+ const brace = content.indexOf("{", named.index);
769
+ return `${content.slice(0, brace + 1)} t,${content.slice(brace + 1)}`;
770
+ }
771
+ let end = 0;
772
+ for (const match of content.matchAll(IMPORT))
773
+ if (inCode(match))
774
+ end = match.index + match[0].length;
775
+ const line = 'import { t } from "@nifrajs/schema"';
776
+ return end === 0
777
+ ? `${line}\n\n${content}`
778
+ : `${content.slice(0, end)}\n${line}${content.slice(end)}`;
779
+ }
780
+ /**
781
+ * What each listed route backend half needs, and the file contents that would apply it. `nifra
782
+ * schemas`, `nifra migrate layout` and the NF-C030 fix share it, so one return type drafts one schema.
783
+ */
784
+ export async function planOutputSchemas(root, files, read, options = {}) {
785
+ const { draft } = options;
786
+ const perFile = new Map();
787
+ for (const file of files) {
788
+ const content = read(file);
789
+ if (content === undefined)
790
+ continue;
791
+ const targets = targetsOf(content);
792
+ if (targets.length > 0)
793
+ perFile.set(file, { content, targets });
794
+ }
795
+ const chosen = (file, target) => draft === undefined || (draft(file, target.schema) && target.declaration === undefined);
796
+ const probe = [...perFile].flatMap(([file, { targets }]) => {
797
+ const read = targets.filter((target) => chosen(file, target));
798
+ return read.length === 0
799
+ ? []
800
+ : [
801
+ {
802
+ file,
803
+ fns: read.map((target) => target.fn),
804
+ schemas: read.filter((t) => t.declaration !== undefined).map((t) => t.schema),
805
+ },
806
+ ];
807
+ });
808
+ const sources = new Map(options.overlay);
809
+ for (const [file, { content }] of perFile)
810
+ sources.set(file, content);
811
+ const { shapes, warnings } = await readShapes(root, probe, sources);
812
+ const entries = [];
813
+ const edits = new Map();
814
+ if (warnings.length > 0)
815
+ return { entries, edits, warnings };
816
+ for (const [file, { content, targets }] of perFile) {
817
+ const mask = codePositionMask(content);
818
+ const replacements = [];
819
+ for (const target of targets) {
820
+ if (!chosen(file, target))
821
+ continue;
822
+ const raw = shapes.get(`${file}#${target.fn}`);
823
+ const declared = shapes.get(`${file}#${target.schema}`);
824
+ const { declaration } = target;
825
+ const line = declaration?.line ?? target.fnLine;
826
+ const entry = (status, dropped = [], reason, edited = false, omitted = [], open = []) => {
827
+ const schema = target.schema;
828
+ const generated = declaration?.hash !== undefined;
829
+ const base = { file, line, schema, status, generated, dropped };
830
+ const item = reason !== undefined ? { ...base, reason } : edited ? { ...base, edited } : base;
831
+ const withOmitted = omitted.length === 0 ? item : { ...item, omitted };
832
+ return entries.push(open.length === 0 ? withOmitted : { ...withOmitted, open }) - 1;
833
+ };
834
+ if (target.annotated) {
835
+ // A `Route.Loader` annotation returns the schema's own input type: comparing the two would
836
+ // always agree, so the schema is reported unchecked rather than passed.
837
+ entry("unreadable", [], `${target.fn} has a type annotation, so its return type is the annotation's, not what it returns. Remove the annotation to have the schema generated and checked, or delete the "nifra:generated" line to own the schema by hand`);
838
+ continue;
839
+ }
840
+ if (raw === undefined) {
841
+ entry("unreadable", [], "TypeScript could not read its type");
842
+ continue;
843
+ }
844
+ const returned = dataShape(raw);
845
+ if (returned.kind === "never") {
846
+ entry("no-data");
847
+ continue;
848
+ }
849
+ const dropped = declared === undefined || declared.kind === "never"
850
+ ? []
851
+ : undeclaredPaths(returned, declared);
852
+ if (declaration === null || (declaration !== undefined && declaration.hash === undefined)) {
853
+ entry("declared", dropped);
854
+ continue;
855
+ }
856
+ if (returned.kind === "json" && returned.reason !== undefined) {
857
+ entry("unreadable", dropped, `the ${target.fn}'s return type is ${returned.reason}: annotate it, or write the schema by hand`);
858
+ continue;
859
+ }
860
+ const body = schemaSource(returned);
861
+ const omitted = omittedPaths(returned);
862
+ const open = openPaths(returned);
863
+ if (declaration === undefined) {
864
+ // The name bound any other way (declared, imported) would clash with the one written here.
865
+ if (new RegExp(`(?<![\\w$.])${target.schema}(?![\\w$])`).test(mask)) {
866
+ entry("missing", [], `\`${target.schema}\` already names something in this file, so no draft was written`);
867
+ continue;
868
+ }
869
+ const at = entry("missing", [], undefined, false, omitted, open);
870
+ const written = declarationText(target.schema, target.fn, body);
871
+ const text = target.insertAt === content.length ? `\n${written}\n` : `${written}\n\n`;
872
+ replacements.push({ start: target.insertAt, end: target.insertAt, text, entry: at });
873
+ }
874
+ else if (declaration.hash === hashOf(body) &&
875
+ schemaTokens(declaration.value) === schemaTokens(body)) {
876
+ entry("current", [], undefined, false, [], open);
877
+ }
878
+ else {
879
+ // The marker still matches what would be written, so the text under it was edited.
880
+ const edited = declaration.hash === hashOf(body);
881
+ const at = entry("stale", dropped, undefined, edited, omitted, open);
882
+ const text = declarationText(target.schema, target.fn, body);
883
+ replacements.push({ start: declaration.start, end: declaration.end, text, entry: at });
884
+ }
885
+ }
886
+ if (replacements.length === 0)
887
+ continue;
888
+ let next = content;
889
+ for (const r of replacements.sort((a, b) => b.start - a.start))
890
+ next = `${next.slice(0, r.start)}${r.text}${next.slice(r.end)}`;
891
+ const imported = withSchemaImport(next);
892
+ if (imported === undefined) {
893
+ for (const r of replacements) {
894
+ const blocked = entries[r.entry];
895
+ if (blocked !== undefined)
896
+ entries[r.entry] = {
897
+ ...blocked,
898
+ reason: "`t` already names something else in this file, so it was left as is",
899
+ };
900
+ }
901
+ continue;
902
+ }
903
+ edits.set(file, imported);
904
+ }
905
+ entries.sort((a, b) => codeUnitOrder(a.file, b.file) || a.line - b.line);
906
+ return { entries, edits, warnings };
907
+ }
908
+ // ---- entry points ------------------------------------------------------------------------------
909
+ const ROUTE_BACKEND_TS = /\.backend\.[cm]?ts$/;
910
+ function routeBackends(dir) {
911
+ const routes = join(dir, "routes");
912
+ if (!existsSync(routes))
913
+ return [];
914
+ return readdirSync(routes, { recursive: true, encoding: "utf8" })
915
+ .map((file) => posix(join("routes", file)))
916
+ .filter((file) => ROUTE_BACKEND_TS.test(file) && !file.split("/").includes("node_modules"))
917
+ .sort(codeUnitOrder);
918
+ }
919
+ /**
920
+ * Every route backend half under `<dir>/routes`: report what each loader's and action's schema needs,
921
+ * and with `write`, write the missing schemas and refresh the stale generated ones. A hand-written
922
+ * schema is never touched; its entry lists the returned fields it drops.
923
+ */
924
+ export async function outputSchemas(dir, options = {}) {
925
+ const write = options.write === true;
926
+ const { entries, edits, warnings } = await planOutputSchemas(dir, routeBackends(dir), (file) => {
927
+ try {
928
+ return readFileSync(join(dir, file), "utf8");
929
+ }
930
+ catch {
931
+ return undefined;
932
+ }
933
+ });
934
+ const written = [];
935
+ let final = entries;
936
+ if (write) {
937
+ for (const [file, content] of edits) {
938
+ writeFileSync(join(dir, file), content);
939
+ written.push(file);
940
+ }
941
+ final = entries.map((entry) => !written.includes(entry.file) || entry.reason !== undefined
942
+ ? entry
943
+ : entry.status === "missing"
944
+ ? { ...entry, status: "generated" }
945
+ : entry.status === "stale"
946
+ ? { ...entry, status: "regenerated" }
947
+ : entry);
948
+ }
949
+ // `check` also fails what a review still owes: a schema not written yet, and one that admits anything.
950
+ const owed = (entry) => entry.status === "missing" || (entry.open?.length ?? 0) > 0;
951
+ return {
952
+ ok: warnings.length === 0 &&
953
+ !final.some((entry) => entry.status === "stale" || (options.check === true && owed(entry))),
954
+ write,
955
+ entries: final,
956
+ written,
957
+ warnings,
958
+ };
959
+ }
960
+ /**
961
+ * The generated schemas among `files` whose route now returns a different shape (NF-C033), and those
962
+ * it could not compare. `warnings` says why nothing could be read at all.
963
+ */
964
+ export async function staleGeneratedSchemas(root, files, read) {
965
+ const marked = files.filter((file) => read(file)?.includes(GENERATED_MARK) === true);
966
+ if (marked.length === 0)
967
+ return { stale: [], unchecked: [], warnings: [] };
968
+ const { entries, warnings } = await planOutputSchemas(root, marked, read);
969
+ return {
970
+ stale: entries.filter((entry) => entry.status === "stale"),
971
+ unchecked: entries.filter((entry) => entry.generated && entry.status === "unreadable"),
972
+ warnings,
973
+ };
974
+ }
975
+ //# sourceMappingURL=output-schemas.js.map