@astryxdesign/cli 0.7.2-canary.fe2c655 → 0.7.2

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 (509) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +175 -154
  3. package/api/build/_adapter.d.mts +8 -0
  4. package/api/build/_adapter.mjs +12 -1
  5. package/api/build/build.doc.mjs +4 -0
  6. package/api/build/help/help.mjs +1 -1
  7. package/api/build/kit/kit.mjs +3 -11
  8. package/api/component/_adapter.d.mts +72 -1
  9. package/api/component/_adapter.mjs +212 -18
  10. package/api/component/component.doc.mjs +6 -1
  11. package/api/component/component.mjs +82 -15
  12. package/api/component/component.test.mjs +80 -0
  13. package/api/component/doc-links.test.mjs +386 -0
  14. package/api/component/integration-export-name.test.mjs +644 -0
  15. package/api/component/list/list.mjs +42 -4
  16. package/api/discover/_adapter.d.mts +132 -12
  17. package/api/discover/_adapter.mjs +488 -27
  18. package/api/discover/_adapter.test.mjs +599 -17
  19. package/api/discover/_catalog-view.d.mts +16 -5
  20. package/api/discover/_catalog-view.mjs +32 -8
  21. package/api/discover/detail/doc/doc.mjs +9 -1
  22. package/api/discover/discover.d.mts +11 -0
  23. package/api/discover/discover.doc.mjs +9 -7
  24. package/api/discover/discover.mjs +121 -19
  25. package/api/discover/discover.type.d.mts +20 -7
  26. package/api/discover/discover.type.mjs +17 -7
  27. package/api/discover/list/list.d.mts +4 -3
  28. package/api/discover/list/list.mjs +17 -10
  29. package/api/docs/_adapter.d.mts +74 -0
  30. package/api/docs/_adapter.mjs +553 -16
  31. package/api/docs/docs.doc.mjs +2 -1
  32. package/api/docs/docs.mjs +2 -1
  33. package/api/docs/docs.test.mjs +5 -2
  34. package/api/docs/integration-tree.test.mjs +22 -0
  35. package/api/docs/integrationDocs.test.mjs +124 -0
  36. package/api/docs/list/list.mjs +12 -2
  37. package/api/docs/reference-blocks.test.mjs +1 -3
  38. package/api/doctor/_adapter.d.mts +108 -0
  39. package/api/doctor/_adapter.mjs +223 -4
  40. package/api/doctor/doctor.d.mts +199 -0
  41. package/api/doctor/doctor.doc.mjs +3 -2
  42. package/api/doctor/doctor.mjs +530 -34
  43. package/api/doctor/doctor.test.mjs +180 -14
  44. package/api/doctor/release-checks.test.mjs +156 -0
  45. package/api/doctor/theme-checks.mjs +17 -4
  46. package/api/doctor/theme-checks.test.mjs +223 -7
  47. package/api/error.d.mts +10 -0
  48. package/api/error.mjs +25 -0
  49. package/api/gap-report/gap-report.d.mts +3 -0
  50. package/api/gap-report/gap-report.doc.mjs +11 -3
  51. package/api/gap-report/gap-report.mjs +219 -52
  52. package/api/gap-report/gap-report.test.mjs +233 -0
  53. package/api/gap-report/gap-report.type.d.mts +41 -0
  54. package/api/gap-report/gap-report.type.mjs +18 -0
  55. package/api/hook/_adapter.mjs +3 -11
  56. package/api/hook/hook.doc.mjs +4 -0
  57. package/api/hook/list/list.d.mts +1 -1
  58. package/api/index.d.mts +2 -0
  59. package/api/index.mjs +2 -0
  60. package/api/init/_adapter.d.mts +22 -0
  61. package/api/init/_adapter.mjs +59 -0
  62. package/api/init/init.d.mts +7 -5
  63. package/api/init/init.doc.mjs +28 -7
  64. package/api/init/init.mjs +28 -4
  65. package/api/init/init.type.d.mts +48 -3
  66. package/api/init/init.type.mjs +37 -3
  67. package/api/init/run/run.d.mts +1 -1
  68. package/api/init/run/run.mjs +17 -6
  69. package/api/init/user/user.d.mts +14 -0
  70. package/api/init/user/user.mjs +87 -0
  71. package/api/init/user/user.test.mjs +232 -0
  72. package/api/integration/_adapter.d.mts +45 -0
  73. package/api/integration/_adapter.mjs +112 -0
  74. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  75. package/api/integration/add-contribution.mjs +8 -4
  76. package/api/integration/add-contribution.test.mjs +4 -2
  77. package/api/integration/authoring-checks.mjs +110 -6
  78. package/api/integration/authoring-checks.test.mjs +808 -0
  79. package/api/integration/authoring-checks.type.d.mts +143 -0
  80. package/api/integration/authoring-checks.type.mjs +74 -2
  81. package/api/integration/check/check.d.mts +96 -0
  82. package/api/integration/check/check.mjs +357 -0
  83. package/api/integration/check/check.test.mjs +489 -0
  84. package/api/integration/integration-check.type.d.mts +118 -0
  85. package/api/integration/integration-check.type.mjs +84 -0
  86. package/api/integration/integrationCheck.doc.d.mts +10 -0
  87. package/api/integration/integrationCheck.doc.mjs +73 -0
  88. package/api/integration/integrationComponentConflicts.doc.mjs +45 -6
  89. package/api/integration/integrationDocConflicts.doc.mjs +22 -5
  90. package/api/integration/pack-check.mjs +364 -120
  91. package/api/integration/pack-check.test.mjs +730 -10
  92. package/api/integration/validate-integration-fixes.test.mjs +67 -0
  93. package/api/integration/validate-integration.d.mts +8 -0
  94. package/api/integration/validate-integration.mjs +63 -9
  95. package/api/integration/validate-integration.test.mjs +88 -8
  96. package/api/integration/validate-integration.type.d.mts +16 -0
  97. package/api/integration/validate-integration.type.mjs +14 -1
  98. package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
  99. package/api/integration/validateIntegration.doc.mjs +19 -3
  100. package/api/search/_adapter.d.mts +11 -0
  101. package/api/search/_adapter.mjs +25 -0
  102. package/api/search/search.d.mts +82 -13
  103. package/api/search/search.doc.mjs +8 -3
  104. package/api/search/search.domain-priority.test.mjs +108 -18
  105. package/api/search/search.integration-check.test.mjs +24 -0
  106. package/api/search/search.mjs +645 -64
  107. package/api/search/search.test.mjs +179 -1
  108. package/api/search/search.type.d.mts +14 -1
  109. package/api/search/search.type.mjs +6 -1
  110. package/api/swizzle/_adapter.mjs +2 -2
  111. package/api/swizzle/copy/copy.mjs +29 -15
  112. package/api/template/template-integration.test.mjs +1 -1
  113. package/api/template/template.test.mjs +120 -3
  114. package/api/theme/_adapter.d.mts +16 -0
  115. package/api/theme/_adapter.mjs +32 -5
  116. package/api/theme/build/build.d.mts +2 -1
  117. package/api/theme/build/build.family.test.mjs +64 -0
  118. package/api/theme/build/build.mjs +250 -38
  119. package/api/theme/build/build.packed-old-core.test.mjs +20 -3
  120. package/api/theme/build/font-warning.mjs +5 -3
  121. package/api/theme/build/font-warning.test.mjs +2 -1
  122. package/api/theme/remove/remove.test.mjs +2 -2
  123. package/api/theme/targets/targets.mjs +3 -10
  124. package/api/theme/themeBuild.doc.mjs +4 -4
  125. package/api/theme/themeTargets.doc.mjs +4 -0
  126. package/api/theme/use/use.test.mjs +1 -1
  127. package/api/upgrade/_adapter.d.mts +40 -2
  128. package/api/upgrade/_adapter.mjs +118 -5
  129. package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
  130. package/api/upgrade/run/run.mjs +35 -2
  131. package/api/upgrade/status/status.d.mts +21 -5
  132. package/api/upgrade/status/status.mjs +45 -8
  133. package/api/upgrade/upgrade.doc.mjs +6 -3
  134. package/api/upgrade/upgrade.mjs +5 -1
  135. package/api/upgrade/upgrade.type.d.mts +23 -0
  136. package/api/upgrade/upgrade.type.mjs +16 -1
  137. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  138. package/assets/codemods/registry.mjs +1 -0
  139. package/assets/codemods/runner.mjs +7 -1
  140. package/assets/codemods/transforms/v0.7.2/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  141. package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
  142. package/assets/codemods/transforms/v0.7.2/migrate-list-edge-compensation-to-edge.mjs +198 -0
  143. package/assets/discover/official-packages.json +367 -0
  144. package/assets/docs/README.md +10 -0
  145. package/assets/docs/author-a-theme.doc.mjs +3 -3
  146. package/assets/docs/browser-support.doc.mjs +1 -1
  147. package/assets/docs/getting-started.doc.mjs +26 -6
  148. package/assets/docs/styling-overview.doc.mjs +1 -1
  149. package/assets/docs/theme.doc.dense.mjs +1 -1
  150. package/assets/docs/theme.doc.mjs +2 -2
  151. package/assets/docs/theme.doc.zh.mjs +1 -1
  152. package/assets/docs/tree/add-a-component.doc.mjs +22 -2
  153. package/assets/docs/tree/add-a-theme.doc.mjs +13 -3
  154. package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
  155. package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
  156. package/assets/docs/tree/block-template.doc.mjs +8 -20
  157. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  158. package/assets/docs/tree/building-blocks.doc.mjs +4 -0
  159. package/assets/docs/tree/check-the-props.doc.mjs +201 -0
  160. package/assets/docs/tree/check-your-docs.doc.mjs +69 -11
  161. package/assets/docs/tree/checks.doc.mjs +32 -24
  162. package/assets/docs/tree/ci-and-containers.doc.mjs +88 -0
  163. package/assets/docs/tree/cli-command.doc.mjs +142 -0
  164. package/assets/docs/tree/cli.doc.mjs +6 -0
  165. package/assets/docs/tree/codemods.doc.mjs +17 -7
  166. package/assets/docs/tree/commands.doc.mjs +7 -1
  167. package/assets/docs/tree/component-family.doc.mjs +8 -6
  168. package/assets/docs/tree/component-imports.doc.mjs +21 -3
  169. package/assets/docs/tree/components.doc.mjs +7 -1
  170. package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
  171. package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
  172. package/assets/docs/tree/configuration.doc.mjs +7 -3
  173. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
  174. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  175. package/assets/docs/tree/describe-the-component.doc.mjs +3 -3
  176. package/assets/docs/tree/docs.doc.mjs +9 -3
  177. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  178. package/assets/docs/tree/document-the-theme.doc.mjs +3 -3
  179. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  180. package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
  181. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  182. package/assets/docs/tree/generate-a-palette.doc.mjs +7 -5
  183. package/assets/docs/tree/global-integrations.doc.mjs +124 -0
  184. package/assets/docs/tree/global.doc.mjs +131 -0
  185. package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
  186. package/assets/docs/tree/install-once.doc.mjs +105 -0
  187. package/assets/docs/tree/integrations.doc.mjs +13 -1
  188. package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
  189. package/assets/docs/tree/layout.doc.mjs +6 -6
  190. package/assets/docs/tree/links.doc.mjs +7 -6
  191. package/assets/docs/tree/migration-setup.doc.mjs +3 -2
  192. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  193. package/assets/docs/tree/migration.doc.mjs +0 -1
  194. package/assets/docs/tree/monorepos.doc.mjs +84 -0
  195. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  196. package/assets/docs/tree/page-template.doc.mjs +4 -8
  197. package/assets/docs/tree/problems.doc.mjs +205 -0
  198. package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
  199. package/assets/docs/tree/publishing.doc.mjs +6 -6
  200. package/assets/docs/tree/quick-start.doc.mjs +53 -27
  201. package/assets/docs/tree/reference.doc.mjs +192 -0
  202. package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
  203. package/assets/docs/tree/replace-a-core-template.doc.mjs +10 -12
  204. package/assets/docs/tree/scaffold.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
  207. package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
  208. package/assets/docs/tree/single-component.doc.mjs +15 -6
  209. package/assets/docs/tree/start-a-template.doc.mjs +18 -8
  210. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  211. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  212. package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
  213. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  214. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  215. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  216. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  217. package/assets/docs/tree/templates.doc.mjs +7 -1
  218. package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
  219. package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
  220. package/assets/docs/tree/themes.doc.mjs +4 -0
  221. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  222. package/assets/docs/tree/troubleshooting.doc.mjs +29 -13
  223. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  224. package/assets/docs/tree/uninstall.doc.mjs +109 -0
  225. package/assets/docs/tree/updates.doc.mjs +92 -0
  226. package/assets/docs/tree/upgrading.doc.mjs +6 -6
  227. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +14 -2
  228. package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
  229. package/assets/docs/tree/versioning.doc.mjs +10 -9
  230. package/assets/docs/tree/who-answers.doc.mjs +119 -0
  231. package/assets/docs/tree/write-the-template-file.doc.mjs +7 -6
  232. package/assets/docs/use-a-theme.doc.mjs +2 -2
  233. package/assets/docs/working-with-ai.doc.mjs +66 -13
  234. package/assets/theme.template.ts +1 -1
  235. package/authoring/codemod/codemod.doc.mjs +7 -3
  236. package/authoring/config/config.doc.mjs +9 -6
  237. package/authoring/config/type.ts +4 -2
  238. package/authoring/debug/parse.d.mts +5 -5
  239. package/authoring/discover/discover.doc.mjs +7 -1
  240. package/authoring/doctypes/_schema.d.mts +8 -3
  241. package/authoring/doctypes/_schema.mjs +1 -0
  242. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  243. package/authoring/doctypes/command/command.doc.mjs +20 -12
  244. package/authoring/doctypes/command/type.ts +14 -0
  245. package/authoring/doctypes/component/component.doc.mjs +30 -27
  246. package/authoring/doctypes/component/type.ts +21 -8
  247. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  248. package/authoring/doctypes/function/function.doc.mjs +9 -8
  249. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  250. package/authoring/doctypes/hook/type.ts +4 -0
  251. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  252. package/authoring/doctypes/reference/reference.doc.mjs +12 -11
  253. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  254. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  255. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  256. package/authoring/doctypes/template/parse.d.mts +6 -2
  257. package/authoring/doctypes/template/parse.mjs +3 -1
  258. package/authoring/doctypes/template/template.doc.mjs +3 -3
  259. package/authoring/doctypes/template/type.ts +22 -0
  260. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  261. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  262. package/authoring/identity/identity.doc.mjs +3 -3
  263. package/authoring/index.d.ts +2 -0
  264. package/authoring/integration/integration.doc.mjs +30 -6
  265. package/authoring/integration/parse.test.mjs +21 -0
  266. package/authoring/integration/schema.d.mts +16 -3
  267. package/authoring/integration/schema.mjs +34 -2
  268. package/authoring/integration/type.ts +12 -0
  269. package/clients/cli/bin/astryx.mjs +20 -3
  270. package/clients/cli/cli-command.test.mjs +166 -0
  271. package/clients/cli/commands/blog.doc.mjs +1 -0
  272. package/clients/cli/commands/build-theme.batch-report.test.mjs +17 -3
  273. package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
  274. package/clients/cli/commands/build-theme.mjs +26 -13
  275. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  276. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  277. package/clients/cli/commands/build.doc.mjs +1 -0
  278. package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
  279. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  280. package/clients/cli/commands/component.doc.mjs +1 -0
  281. package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
  282. package/clients/cli/commands/discover.doc.mjs +9 -1
  283. package/clients/cli/commands/discover.mjs +144 -93
  284. package/clients/cli/commands/discover.no-source.test.mjs +448 -33
  285. package/clients/cli/commands/discover.sources.test.mjs +20 -1
  286. package/clients/cli/commands/docs.doc.mjs +1 -0
  287. package/clients/cli/commands/docs.mjs +49 -19
  288. package/clients/cli/commands/docs.test.mjs +2 -2
  289. package/clients/cli/commands/doctor-integration-components.doc.mjs +19 -4
  290. package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
  291. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
  292. package/clients/cli/commands/doctor-integration-validate.doc.mjs +22 -2
  293. package/clients/cli/commands/doctor-integration.test.mjs +888 -3
  294. package/clients/cli/commands/doctor.doc.mjs +12 -2
  295. package/clients/cli/commands/doctor.mjs +292 -7
  296. package/clients/cli/commands/doctor.test.mjs +10 -4
  297. package/clients/cli/commands/gap-report.doc.mjs +15 -2
  298. package/clients/cli/commands/gap-report.mjs +24 -2
  299. package/clients/cli/commands/gap-report.test.mjs +79 -0
  300. package/clients/cli/commands/hook.doc.mjs +1 -0
  301. package/clients/cli/commands/init.behavior.test.mjs +5 -0
  302. package/clients/cli/commands/init.doc.mjs +29 -3
  303. package/clients/cli/commands/integration-add.doc.mjs +7 -0
  304. package/clients/cli/commands/integration-authoring.test.mjs +49 -3
  305. package/clients/cli/commands/integration-check.doc.mjs +93 -0
  306. package/clients/cli/commands/integration-check.test.mjs +441 -0
  307. package/clients/cli/commands/integration-pack.doc.mjs +8 -1
  308. package/clients/cli/commands/integration-verify.doc.mjs +8 -1
  309. package/clients/cli/commands/integration.doc.mjs +6 -2
  310. package/clients/cli/commands/integration.mjs +119 -7
  311. package/clients/cli/commands/json-contract.test.mjs +11 -2
  312. package/clients/cli/commands/manifest.doc.mjs +1 -0
  313. package/clients/cli/commands/outside-project.test.mjs +774 -0
  314. package/clients/cli/commands/search.doc.mjs +1 -0
  315. package/clients/cli/commands/search.mjs +16 -11
  316. package/clients/cli/commands/search.test.mjs +429 -296
  317. package/clients/cli/commands/setup-nudge.test.mjs +102 -2
  318. package/clients/cli/commands/swizzle.doc.mjs +7 -0
  319. package/clients/cli/commands/swizzle.mjs +5 -3
  320. package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
  321. package/clients/cli/commands/template.doc.mjs +1 -0
  322. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  323. package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
  324. package/clients/cli/commands/theme-add.doc.mjs +8 -1
  325. package/clients/cli/commands/theme-build.doc.mjs +3 -2
  326. package/clients/cli/commands/theme-eject.doc.mjs +1 -0
  327. package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
  328. package/clients/cli/commands/theme-list.doc.mjs +1 -0
  329. package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
  330. package/clients/cli/commands/theme-remove.doc.mjs +7 -0
  331. package/clients/cli/commands/theme-targets.doc.mjs +1 -0
  332. package/clients/cli/commands/theme-template.doc.mjs +1 -0
  333. package/clients/cli/commands/theme-use.doc.mjs +7 -0
  334. package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
  335. package/clients/cli/commands/upgrade.doc.mjs +22 -1
  336. package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
  337. package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
  338. package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
  339. package/clients/cli/index.mjs +112 -16
  340. package/clients/cli/lib/cli-error.mjs +4 -1
  341. package/clients/cli/lib/component-format.test.mjs +1 -1
  342. package/clients/cli/lib/define-command.mjs +2 -2
  343. package/clients/cli/lib/json-shim.mjs +3 -1
  344. package/clients/cli/lib/manifest.d.ts +9 -0
  345. package/clients/cli/lib/manifest.mjs +17 -1
  346. package/clients/cli/lib/manifest.test.mjs +25 -0
  347. package/foundation/agent-docs/agent-doc-state.d.mts +35 -6
  348. package/foundation/agent-docs/agent-doc-state.mjs +66 -6
  349. package/foundation/agent-docs/agent-docs.d.mts +4 -3
  350. package/foundation/agent-docs/agent-docs.mjs +11 -9
  351. package/foundation/agent-docs/computer-agent-text.d.mts +133 -0
  352. package/foundation/agent-docs/computer-agent-text.mjs +623 -0
  353. package/foundation/agent-docs/computer-agent-text.test.mjs +602 -0
  354. package/foundation/config/project.d.mts +35 -3
  355. package/foundation/config/project.mjs +226 -7
  356. package/foundation/config/project.test.mjs +66 -0
  357. package/foundation/config/theme-state.mjs +5 -4
  358. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  359. package/foundation/discovery/cli-self-docs.mjs +20 -3
  360. package/foundation/discovery/cli-self-docs.test.mjs +3 -2
  361. package/foundation/discovery/component-discovery.d.mts +190 -10
  362. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  363. package/foundation/discovery/component-discovery.mjs +864 -56
  364. package/foundation/discovery/component-discovery.test.mjs +96 -0
  365. package/foundation/discovery/component-replacement.mjs +20 -1
  366. package/foundation/discovery/component-replacement.test.mjs +40 -0
  367. package/foundation/discovery/copied-passages.d.mts +216 -0
  368. package/foundation/discovery/copied-passages.mjs +720 -0
  369. package/foundation/discovery/copied-passages.test.mjs +124 -0
  370. package/foundation/discovery/doc-consistency.d.mts +34 -0
  371. package/foundation/discovery/doc-consistency.mjs +513 -0
  372. package/foundation/discovery/doc-consistency.test.mjs +347 -0
  373. package/foundation/discovery/doc-fluff.d.mts +112 -0
  374. package/foundation/discovery/doc-fluff.mjs +328 -0
  375. package/foundation/discovery/doc-fluff.test.mjs +142 -0
  376. package/foundation/discovery/doc-types.d.mts +157 -0
  377. package/foundation/discovery/doc-types.mjs +1231 -0
  378. package/foundation/discovery/doc-types.test.mjs +744 -0
  379. package/foundation/discovery/docs-discovery.d.mts +88 -0
  380. package/foundation/discovery/docs-discovery.mjs +83 -2
  381. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  382. package/foundation/discovery/integration-doc-files.mjs +231 -0
  383. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  384. package/foundation/discovery/module-exports.d.mts +27 -0
  385. package/foundation/discovery/module-exports.mjs +356 -31
  386. package/foundation/discovery/module-exports.test.mjs +172 -0
  387. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  388. package/foundation/discovery/package-tsconfig.mjs +228 -0
  389. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  390. package/foundation/discovery/project-typescript.d.mts +32 -0
  391. package/foundation/discovery/project-typescript.mjs +89 -0
  392. package/foundation/discovery/prop-drift.d.mts +297 -0
  393. package/foundation/discovery/prop-drift.mjs +2428 -0
  394. package/foundation/discovery/prop-drift.test.mjs +2337 -0
  395. package/foundation/discovery/runner-allowlist.d.mts +40 -0
  396. package/foundation/discovery/runner-allowlist.mjs +53 -0
  397. package/foundation/discovery/stray-pointers.d.mts +52 -0
  398. package/foundation/discovery/stray-pointers.mjs +293 -0
  399. package/foundation/discovery/stray-pointers.test.mjs +86 -0
  400. package/foundation/discovery/template-adapter.mjs +74 -5
  401. package/foundation/discovery/template-needs.mjs +5 -2
  402. package/foundation/discovery/theme-imports.mjs +8 -4
  403. package/foundation/discovery/theme-imports.test.mjs +1 -1
  404. package/foundation/discovery/type-match.d.mts +53 -0
  405. package/foundation/discovery/type-match.mjs +660 -0
  406. package/foundation/discovery/type-match.test.mjs +169 -0
  407. package/foundation/discovery/type-text.d.mts +68 -0
  408. package/foundation/discovery/type-text.mjs +658 -0
  409. package/foundation/discovery/type-text.test.mjs +196 -0
  410. package/foundation/discovery/unresolved-references.d.mts +62 -0
  411. package/foundation/discovery/unresolved-references.mjs +790 -0
  412. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  413. package/foundation/doc-compiler/doc-loads.test.mjs +51 -1
  414. package/foundation/doc-compiler/inputs.mjs +27 -1
  415. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  416. package/foundation/doc-compiler/links.d.mts +41 -5
  417. package/foundation/doc-compiler/links.mjs +167 -28
  418. package/foundation/doc-compiler/links.test.mjs +97 -4
  419. package/foundation/doc-compiler/read.d.mts +1 -1
  420. package/foundation/doc-compiler/tree.mjs +30 -1
  421. package/foundation/doc-compiler/tree.test.mjs +8 -1
  422. package/foundation/env/cli-command.d.mts +83 -0
  423. package/foundation/env/cli-command.mjs +152 -0
  424. package/foundation/env/cli-command.test.mjs +129 -0
  425. package/foundation/env/node-version.test.mjs +15 -3
  426. package/foundation/env/package-manager.d.mts +93 -9
  427. package/foundation/env/package-manager.global.test.mjs +66 -0
  428. package/foundation/env/package-manager.mjs +184 -13
  429. package/foundation/env/package-manager.test.mjs +341 -15
  430. package/foundation/env/project-scope.d.mts +9 -0
  431. package/foundation/env/project-scope.mjs +33 -0
  432. package/foundation/env/search-path.d.mts +42 -0
  433. package/foundation/env/search-path.mjs +205 -0
  434. package/foundation/env/search-path.test.mjs +122 -0
  435. package/foundation/env/user-dirs.d.mts +39 -0
  436. package/foundation/env/user-dirs.mjs +93 -0
  437. package/foundation/env/user-dirs.test.mjs +72 -0
  438. package/foundation/env/user-settings.d.mts +46 -0
  439. package/foundation/env/user-settings.mjs +106 -0
  440. package/foundation/env/user-settings.test.mjs +64 -0
  441. package/foundation/fs/file-protection.mjs +15 -1
  442. package/foundation/fs/module-loader.d.mts +2 -1
  443. package/foundation/fs/module-loader.mjs +60 -4
  444. package/foundation/fs/module-loader.stdout.test.mjs +3 -2
  445. package/foundation/fs/module-loader.test.mjs +194 -0
  446. package/foundation/fs/path-compare.d.mts +33 -0
  447. package/foundation/fs/path-compare.mjs +73 -0
  448. package/foundation/fs/paths.d.mts +57 -1
  449. package/foundation/fs/paths.mjs +131 -3
  450. package/foundation/fs/paths.test.mjs +10 -1
  451. package/foundation/integrations/computer-integrations.d.mts +78 -0
  452. package/foundation/integrations/computer-integrations.mjs +316 -0
  453. package/foundation/integrations/computer-integrations.test.mjs +292 -0
  454. package/foundation/integrations/contribution-inventory.d.mts +11 -0
  455. package/foundation/integrations/contribution-inventory.mjs +56 -3
  456. package/foundation/integrations/integration-warnings.mjs +3 -2
  457. package/foundation/integrations/integration-warnings.test.mjs +8 -9
  458. package/foundation/integrations/integrations.d.mts +25 -1
  459. package/foundation/integrations/integrations.mjs +46 -3
  460. package/foundation/integrations/issue.ts +6 -0
  461. package/foundation/integrations/validate-contributions.d.mts +2 -0
  462. package/foundation/integrations/validate-contributions.mjs +41 -1
  463. package/foundation/release/agent-text-worker.d.mts +4 -0
  464. package/foundation/release/agent-text-worker.mjs +52 -0
  465. package/foundation/release/cache.d.mts +310 -0
  466. package/foundation/release/cache.mjs +1216 -0
  467. package/foundation/release/cache.test.mjs +920 -0
  468. package/foundation/release/choose.d.mts +207 -0
  469. package/foundation/release/choose.mjs +677 -0
  470. package/foundation/release/choose.test.mjs +427 -0
  471. package/foundation/release/core-worker.d.mts +4 -0
  472. package/foundation/release/core-worker.mjs +38 -0
  473. package/foundation/release/global-state.d.mts +259 -0
  474. package/foundation/release/global-state.mjs +472 -0
  475. package/foundation/release/global-state.test.mjs +381 -0
  476. package/foundation/release/handoff.d.mts +60 -0
  477. package/foundation/release/handoff.e2e.test.mjs +278 -0
  478. package/foundation/release/handoff.mjs +357 -0
  479. package/foundation/release/lock-lease.d.mts +23 -0
  480. package/foundation/release/lock-lease.mjs +80 -0
  481. package/foundation/release/notices.d.mts +253 -0
  482. package/foundation/release/notices.mjs +596 -0
  483. package/foundation/release/notices.test.mjs +565 -0
  484. package/foundation/release/npm-guard.d.mts +4 -0
  485. package/foundation/release/npm-guard.mjs +126 -0
  486. package/foundation/release/project.d.mts +141 -0
  487. package/foundation/release/project.mjs +466 -0
  488. package/foundation/release/project.test.mjs +288 -0
  489. package/foundation/release/refresh-worker.d.mts +4 -0
  490. package/foundation/release/refresh-worker.mjs +27 -0
  491. package/foundation/release/run-context.d.mts +47 -0
  492. package/foundation/release/run-context.mjs +149 -0
  493. package/foundation/release/update-notice-env.d.mts +28 -0
  494. package/foundation/release/update-notice-env.mjs +39 -0
  495. package/foundation/response/base.d.ts +26 -3
  496. package/foundation/response/error-codes.d.mts +3 -1
  497. package/foundation/response/error-codes.doc.mjs +12 -0
  498. package/foundation/response/error-codes.mjs +17 -0
  499. package/foundation/response/error-codes.test.mjs +2 -0
  500. package/foundation/response/json-contract.test.mjs +7 -2
  501. package/foundation/response/json.mjs +4 -1
  502. package/foundation/response/response-types.doc.mjs +22 -11
  503. package/foundation/response/response-types.doc.test.mjs +46 -0
  504. package/foundation/response/response.doc.mjs +3 -3
  505. package/foundation/xle/browser-imports.test.mjs +74 -0
  506. package/foundation/xle/registry.mjs +4 -0
  507. package/foundation/xle/registry.test.mjs +28 -0
  508. package/package.json +11 -11
  509. package/scripts/postinstall.mjs +24 -5
@@ -0,0 +1,2428 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Compare a documented prop table with the props the TypeScript source
5
+ * declares for the export it describes.
6
+ *
7
+ * The compiler is the project's own `typescript`. Without one it can use, the
8
+ * comparison is skipped and says why. Every finding is a warning.
9
+ *
10
+ * `comparePropDocs` finds each component doc's prop tables first: a doc's
11
+ * own `props` document the export its `name` names; each entry of a
12
+ * multi-component doc documents its own export, which a member record
13
+ * compares against its own source. Hook entries (`use…`) document parameters
14
+ * and returns, not props, and are not compared.
15
+ *
16
+ * Each table (a doc's `props`, or a multi-component doc's entry, with the
17
+ * export it documents and the files that may hold it) is compared with that
18
+ * export's props type:
19
+ * - `not_in_code`: a documented prop the props type does not have (a
20
+ * `data-*`-style name an index signature accepts counts as present);
21
+ * - `not_in_doc`: a prop the package's own source declares, or builds with a
22
+ * mapped type from its own keys, that the table does not list. Props from
23
+ * outside the package (React, the DOM, another package's types or keys) are
24
+ * never required, and neither are `className`, `style`, `xstyle`,
25
+ * `data-testid`, `ref`, `key`, or a prop whose declaration carries an
26
+ * `internal` JSDoc tag;
27
+ * - `required_mismatch`: `required` disagrees with whether every caller must
28
+ * pass the prop (one only some members of the props type require, or that
29
+ * the package's `defaultProps` fills, is optional, as it is to JSX
30
+ * callers; an entry that names several props claims its `required` for
31
+ * each);
32
+ * - `type_mismatch`: for a prop declared in the package, the documented type
33
+ * differs from the code's where both sides read exactly (type-match.mjs
34
+ * states the rules); every other type is listed as not compared;
35
+ * - `default_mismatch`: the doc's `default` and the literal default the
36
+ * component writes in its props parameter's destructuring (`size = 'md'`)
37
+ * differ, quotes and whitespace aside; where both write a default and
38
+ * either is not a literal, it is listed under `typesNotCompared`.
39
+ *
40
+ * @input Component records (name, docPath, sourcePath), or prop tables, the
41
+ * package root, and the directories to resolve `typescript` from.
42
+ * @output A PropDriftReport.
43
+ * @position Shared by `astryx doctor integration components` and the Astryx
44
+ * repository's test over Core's own component docs.
45
+ */
46
+
47
+ import * as fs from 'node:fs';
48
+ import * as path from 'node:path';
49
+ import {folderSourceFor} from './component-discovery.mjs';
50
+ import {loadComponentDoc} from './component-loader.mjs';
51
+ import {readPackageTsconfig} from './package-tsconfig.mjs';
52
+ import {
53
+ resolveProjectTypeScript,
54
+ TYPESCRIPT_FIX,
55
+ unusableTypeScriptReason,
56
+ } from './project-typescript.mjs';
57
+ import {judgeType} from './type-match.mjs';
58
+ import {typeTextTooOld} from './type-text.mjs';
59
+ import {
60
+ unresolvedGlobals,
61
+ unresolvedReference,
62
+ } from './unresolved-references.mjs';
63
+
64
+ /**
65
+ * @typedef {'not_in_code' | 'not_in_doc' | 'required_mismatch' | 'type_mismatch' | 'default_mismatch'} PropDriftKind
66
+ */
67
+
68
+ /**
69
+ * @typedef {object} PropDriftFinding
70
+ * @property {string} component the documented component (export) name
71
+ * @property {string} prop
72
+ * @property {PropDriftKind} kind
73
+ * @property {'warning'} severity
74
+ * @property {string} message
75
+ * @property {string} doc the doc file, relative to the package root
76
+ * @property {string} source the source file, relative to the package root
77
+ * @property {string} [docType] the documented type, as written
78
+ * @property {string} [codeType] the type as the source declares it
79
+ * @property {boolean} [docRequired]
80
+ * @property {boolean} [codeRequired]
81
+ * @property {string} [docDefault] the documented default, as written
82
+ * @property {string} [codeDefault] the literal default the code writes
83
+ * @property {string} [fix] what to change, ready to paste
84
+ */
85
+
86
+ /**
87
+ * @typedef {object} PropDriftUnchecked
88
+ * @property {string} component
89
+ * @property {string} doc the doc file, relative to the package root
90
+ * @property {string} reason why its props could not be read
91
+ */
92
+
93
+ /**
94
+ * @typedef {object} PropDriftTypesNotCompared
95
+ * @property {string} component
96
+ * @property {string} doc the doc file, relative to the package root
97
+ * @property {string} reason why: not a form the check judges exactly, a
98
+ * field the prop's variants give different types, a standard prop
99
+ * (`className`, `style`, …), a prop declared outside the package, or a
100
+ * default that is not a literal on one side
101
+ * @property {string[]} props documented props whose types, or defaults, were
102
+ * not compared
103
+ */
104
+
105
+ /**
106
+ * @typedef {object} PropDriftReport
107
+ * @property {'checked' | 'skipped'} status `skipped` means nothing was
108
+ * compared, and `reason` says why
109
+ * @property {string} [reason]
110
+ * @property {string} [typescript] the version of the compiler that read the props
111
+ * @property {number} components how many documented prop tables were compared
112
+ * @property {number} typesCompared how many documented prop types were judged
113
+ * @property {string} [typesSkipped] why no prop type was judged
114
+ * @property {PropDriftFinding[]} findings
115
+ * @property {PropDriftUnchecked[]} unchecked documented components whose props
116
+ * could not be read from the source
117
+ * @property {Array<PropDriftUnchecked & {props: string[]}>} [propsNotChecked]
118
+ * props a mapped type builds from keys the check can't trace, which a doc
119
+ * leaving out draws no warning for
120
+ * @property {PropDriftTypesNotCompared[]} typesNotCompared every other
121
+ * documented prop type, grouped by why it was not judged
122
+ */
123
+
124
+ /**
125
+ * @typedef {object} PropDriftTable
126
+ * @property {string} doc the doc file, relative to the package root
127
+ * @property {string} name the export the table documents
128
+ * @property {any[]} props the table's entries
129
+ * @property {string[]} sources the files that may hold the export, in order
130
+ */
131
+
132
+ /**
133
+ * @typedef {object} PropDriftComponent
134
+ * @property {string} name
135
+ * @property {string} docPath absolute path to the component's `.doc.*` file
136
+ * @property {string | null} sourcePath absolute path to its same-stem
137
+ * source, or null when no file in the package declares it (a family member
138
+ * only another folder or built output defines)
139
+ */
140
+
141
+ /**
142
+ * Props a doc never needs to list: the internal and styling props the
143
+ * `ComponentPropDoc` authoring guidance says to skip, `ref`, which Core's
144
+ * docs also leave out, and `key`, which React keeps for itself.
145
+ */
146
+ export const PROPS_NOT_DOCUMENTED = Object.freeze([
147
+ 'className',
148
+ 'style',
149
+ 'xstyle',
150
+ 'data-testid',
151
+ 'ref',
152
+ 'key',
153
+ ]);
154
+
155
+ /** The guidance's internal props: a declaration tagged `@internal`. */
156
+ const SKIP_TAGS = new Set(['internal']);
157
+ /** Why a documented prop type was not judged (`typesNotCompared`). */
158
+ const NOT_EXACT = 'not a form the check judges exactly';
159
+ /** A field documented with one variant's type, where they differ. */
160
+ const VARIANTS = "the prop's variants give this field different types";
161
+
162
+ /** A default the code writes that is not a literal (a constant, a call). */
163
+ const NOT_LITERAL = Symbol('not a literal');
164
+
165
+ /** Why a documented default was not compared with the code's. */
166
+ const CODE_DEFAULT_NOT_LITERAL =
167
+ 'its default in the code is not a literal, so the documented default was not compared';
168
+ const DOC_DEFAULT_NOT_LITERAL =
169
+ "its documented default is not a literal, so it was not compared with the code's";
170
+ /** @type {(name: string, code?: CodeProp) => string} why a prop is not judged */
171
+ const outside = (name, code) =>
172
+ code?.untraced
173
+ ? NOT_EXACT
174
+ : PROPS_NOT_DOCUMENTED.includes(name)
175
+ ? 'standard prop, not compared'
176
+ : 'declared outside the package, not compared';
177
+ const HOOK_NAME = /^use[A-Z0-9]/;
178
+
179
+ /** The line both surfaces print when the compiler is missing. */
180
+ export const TYPESCRIPT_MISSING_REASON = `TypeScript is not installed in this project, so documented props were not compared with the code. ${TYPESCRIPT_FIX}`;
181
+
182
+ /**
183
+ * @template T
184
+ * @param {() => T} compute
185
+ * @returns {() => T}
186
+ */
187
+ function memo(compute) {
188
+ /** @type {{value: T} | null} */
189
+ let cached = null;
190
+ return () => {
191
+ if (cached == null) cached = {value: compute()};
192
+ return cached.value;
193
+ };
194
+ }
195
+
196
+ /** @type {WeakMap<Function, (file: string) => boolean>} */
197
+ const MINE = new WeakMap();
198
+
199
+ /**
200
+ * Whose type names the walk checks: the package's own files and any that
201
+ * aren't a dependency's. The same test for the same own-file test.
202
+ * @param {(file: string) => boolean} isOwnFile
203
+ */
204
+ function mineFor(isOwnFile) {
205
+ let mine = MINE.get(isOwnFile);
206
+ if (!mine) {
207
+ mine = (/** @type {string} */ file) =>
208
+ isOwnFile(file) || !/[\\/]node_modules[\\/]/.test(file);
209
+ MINE.set(isOwnFile, mine);
210
+ }
211
+ return mine;
212
+ }
213
+
214
+ /**
215
+ * A memoized "is this file part of the package's own source?" test.
216
+ * @param {string} root realpath of the package root
217
+ */
218
+ function ownFileTest(root) {
219
+ /** @type {Map<string, boolean>} */
220
+ const cache = new Map();
221
+ return (/** @type {string} */ file) => {
222
+ let own = cache.get(file);
223
+ if (own !== undefined) return own;
224
+ let real = file;
225
+ try {
226
+ real = fs.realpathSync(file);
227
+ } catch {
228
+ // A lib or virtual file: compare as given.
229
+ }
230
+ const rel = path.relative(root, real);
231
+ own =
232
+ rel !== '' &&
233
+ !rel.startsWith('..') &&
234
+ !path.isAbsolute(rel) &&
235
+ !rel.split(path.sep).includes('node_modules');
236
+ cache.set(file, own);
237
+ return own;
238
+ };
239
+ }
240
+
241
+ /**
242
+ * @typedef {object} CodeProp
243
+ * @property {string} name
244
+ * @property {boolean} required whether every caller must pass it: false
245
+ * when a member of the props type, an overload, or the package's
246
+ * `defaultProps` lets a caller leave it out
247
+ * @property {boolean} own declared in the package's own source, or built
248
+ * from keys it writes
249
+ * @property {boolean} [untraced] built from keys the check can't trace
250
+ * @property {boolean} skipTagged its declaration carries an internal JSDoc tag
251
+ * @property {string[]} declared the type as written, per own declaration
252
+ * @property {() => string[][]} renderings the compiler's renderings, computed on demand
253
+ * @property {() => any[]} types the prop's type in each member of the props type
254
+ * @property {boolean} intersected one member declares it more than once (an
255
+ * intersection), so no one declaration's text is its type
256
+ * @property {() => any[] | null} indexed the type an index signature gives
257
+ * the name in each member that lacks the prop; null when two signatures
258
+ * in one member take it
259
+ * @property {any} location where names in the prop's type resolve
260
+ */
261
+
262
+ /**
263
+ * Read the props of one export.
264
+ * @param {any} ts
265
+ * @param {any} program
266
+ * @param {any} checker
267
+ * @param {string} sourcePath
268
+ * @param {string} exportName
269
+ * @param {(file: string) => boolean} isOwnFile
270
+ * @param {import('./unresolved-references.mjs').UnresolvedReference | null} ambient
271
+ * what the program's other roots cannot resolve, which any component may
272
+ * use
273
+ * @returns {{ok: true, props: Map<string, CodeProp>, accepts: (name: string) => any, defaults?: Map<string, string | number | boolean | null | symbol>} | {ok: false, reason: string}}
274
+ */
275
+ function readCodeProps(
276
+ ts,
277
+ program,
278
+ checker,
279
+ sourcePath,
280
+ exportName,
281
+ isOwnFile,
282
+ ambient,
283
+ ) {
284
+ const sourceFile = program.getSourceFile(sourcePath);
285
+ const sourceName = path.basename(sourcePath);
286
+ if (!sourceFile) {
287
+ return {ok: false, reason: `the compiler could not read ${sourceName}`};
288
+ }
289
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
290
+ const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
291
+ const stem = sourceName.replace(/\.[cm]?[jt]sx?$/, '');
292
+ let symbol =
293
+ exports.find(/** @param {any} s */ s => s.name === exportName) ??
294
+ (stem === exportName
295
+ ? exports.find(/** @param {any} s */ s => s.name === 'default')
296
+ : undefined);
297
+ if (!symbol) {
298
+ return {
299
+ ok: false,
300
+ reason: `${sourceName} has no export named ${exportName}`,
301
+ };
302
+ }
303
+ if (symbol.flags & ts.SymbolFlags.Alias) {
304
+ symbol = checker.getAliasedSymbol(symbol);
305
+ }
306
+ // An import that does not resolve, in the files the component comes from,
307
+ // can make any part of its props a guess, whatever the code looks like.
308
+ // Some TypeScript versions read such a name as a plain `any`, so this comes
309
+ // first.
310
+ const own = [sourceFile, ...(symbol.declarations ?? [])]
311
+ .map(/** @param {any} n */ n => n.getSourceFile())
312
+ .filter(/** @param {any} sf */ sf => isOwnFile(sf.fileName));
313
+ // Type names are checked in the files that aren't a dependency's. One
314
+ // test per run, so the walk reads each file once for every component.
315
+ const mine = mineFor(isOwnFile);
316
+ const found = unresolvedReference(
317
+ ts,
318
+ program,
319
+ checker,
320
+ own,
321
+ () => true,
322
+ mine,
323
+ );
324
+ // A reference that doesn't resolve names what to install; a type the
325
+ // compiler can't give in the component's own files may only show a broken
326
+ // global one.
327
+ const missing = found && found.how !== 'used' ? found : (ambient ?? found);
328
+ if (missing) {
329
+ const where =
330
+ path.relative(path.dirname(sourcePath), missing.file) || sourceName;
331
+ return {
332
+ ok: false,
333
+ reason:
334
+ missing.how === 'used'
335
+ ? `${exportName} depends on ${missing.name} (used in ${where}), a type the compiler can't work out, so its props would be a guess. Check that the package's dependencies are installed and its types compile, then run this check again`
336
+ : `${exportName} depends on ${missing.name} (${missing.how} in ${where}), which the compiler could not resolve, usually a dependency that is not installed. Install the package's dependencies and run this check again`,
337
+ };
338
+ }
339
+ const type = checker.getTypeOfSymbolAtLocation(symbol, sourceFile);
340
+ // Each member of a union (`ComponentType<P>`: a class or a function).
341
+ const signatures = (type.isUnion() ? type.types : [type]).flatMap(
342
+ /** @param {any} t */ t => [
343
+ ...t.getCallSignatures(),
344
+ ...t.getConstructSignatures(),
345
+ ],
346
+ );
347
+ if (signatures.length === 0) {
348
+ // An `any` the source does not write comes from a type that does not
349
+ // resolve, such as `forwardRef` when React's types are not installed.
350
+ const writtenAny =
351
+ symbol.valueDeclaration?.type?.kind === ts.SyntaxKind.AnyKeyword;
352
+ const unresolved =
353
+ !writtenAny &&
354
+ (type.flags & ts.TypeFlags.Any ||
355
+ hasUnresolvedPart(ts, checker, type, 0, new Set()));
356
+ return {
357
+ ok: false,
358
+ reason: unresolved
359
+ ? `${exportName}'s own type includes a type the compiler could not resolve, usually an import whose package is not installed (such as React's types, for a forwardRef or memo component). Install the package's dependencies and run this check again`
360
+ : writtenAny
361
+ ? `${exportName} is typed \`any\`, so it has no props to compare`
362
+ : `${exportName} in ${sourceName} is not a component (it cannot be called with props)`,
363
+ };
364
+ }
365
+ const unresolved = {
366
+ ok: /** @type {const} */ (false),
367
+ reason: `${exportName}'s props type includes a type the compiler could not resolve, usually an import whose package is not installed. Install the package's dependencies and run this check again`,
368
+ };
369
+ // Every overload: its props type joins the others as a member, as a
370
+ // union's members do. An overload that takes no props makes every prop
371
+ // optional.
372
+ /** @type {any[]} */
373
+ const propsTypes = [];
374
+ let propless = false;
375
+ for (const signature of signatures) {
376
+ const parameter = signature.getParameters()[0];
377
+ if (parameter) {
378
+ propsTypes.push(checker.getTypeOfSymbolAtLocation(parameter, sourceFile));
379
+ } else {
380
+ propless = true;
381
+ }
382
+ }
383
+ /** @type {Map<string, CodeProp>} */
384
+ const props = new Map();
385
+ if (propsTypes.length === 0) return {ok: true, props, accepts: () => []};
386
+
387
+ // Props that are `any`, written so or made from it (`FC<any>`, or
388
+ // `forwardRef<E, any>`'s `Omit<any, 'ref'>`).
389
+ if (propsTypes.some(propsType => isAnyProps(ts, checker, propsType))) {
390
+ return {
391
+ ok: false,
392
+ reason: `${exportName}'s props are typed \`any\`, so there is nothing to compare them with`,
393
+ };
394
+ }
395
+ if (
396
+ propsTypes.some(propsType =>
397
+ hasUnresolvedPart(ts, checker, propsType, 0, new Set()),
398
+ )
399
+ ) {
400
+ return unresolved;
401
+ }
402
+
403
+ const members = propsTypes
404
+ .flatMap(propsType => (propsType.isUnion() ? propsType.types : [propsType]))
405
+ .filter(
406
+ /** @param {any} t */
407
+ t => !(t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)),
408
+ );
409
+ // A generic component whose props take their names from a type parameter
410
+ // (the element it renders: `Omit<ComponentPropsWithoutRef<E>, 'as'>`, or a
411
+ // bare `props: P`) accepts names that are known only where it is used.
412
+ const generic = signatures.some(
413
+ /** @param {any} signature */ signature =>
414
+ (signature.getTypeParameters?.() ?? []).length > 0,
415
+ );
416
+ // Props that refer to a type parameter and show the compiler no members
417
+ // at all would draw a finding for every documented name: listed too.
418
+ const unnamed =
419
+ members.some(member => isDeferred(ts, checker, member, true)) &&
420
+ members.every(
421
+ member =>
422
+ checker.getPropertiesOfType(member).length === 0 &&
423
+ (checker.getIndexInfosOfType?.(member) ?? []).length === 0,
424
+ );
425
+ if (
426
+ generic &&
427
+ (unnamed ||
428
+ members.some(member => namesFromTypeParameter(ts, checker, member, 0)))
429
+ ) {
430
+ return {
431
+ ok: false,
432
+ reason: `${exportName}'s props take some of their names from a type parameter, such as the element it renders, so which props it accepts is known only where it is used`,
433
+ };
434
+ }
435
+ // Each member of a prop's type is printed on its own, and long prints are
436
+ // truncated: a type that expands to a huge structure (StyleX styles, say)
437
+ // never matches a documented type anyway, and printing it whole can take
438
+ // gigabytes.
439
+ const flags =
440
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType |
441
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
442
+
443
+ // `defaultProps` the package sets (a class's static field, one assigned to a
444
+ // function, or the wrapped component's through up to two layers of `memo`
445
+ // and `lazy`, as React's JSX types read it) lets JSX callers leave its props
446
+ // out; `any` fills every prop. React's own types declare an optional one on
447
+ // every component type.
448
+ let holder = type;
449
+ for (let i = 0; i < 2 && checker.getPropertyOfType(holder, '$$typeof'); i++) {
450
+ const inner =
451
+ checker.getPropertyOfType(holder, 'type') ??
452
+ checker.getPropertyOfType(holder, '_result');
453
+ if (!inner) break;
454
+ holder = checker.getTypeOfSymbolAtLocation(inner, sourceFile);
455
+ }
456
+ const defaultProps = checker.getPropertyOfType(holder, 'defaultProps');
457
+ const defaults = defaultProps?.declarations?.some(
458
+ /** @param {any} d */ d => isOwnFile(d.getSourceFile().fileName),
459
+ )
460
+ ? checker.getTypeOfSymbolAtLocation(defaultProps, sourceFile)
461
+ : null;
462
+ const fillsAll = !!defaults && !!(defaults.flags & ts.TypeFlags.Any);
463
+ const defaulted = new Set(
464
+ defaults
465
+ ? checker
466
+ .getPropertiesOfType(defaults)
467
+ .map(/** @param {any} s */ s => s.name)
468
+ : [],
469
+ );
470
+
471
+ /** @type {Map<string, {symbols: any[], optional: number}>} */
472
+ const byName = new Map();
473
+ for (const member of members) {
474
+ for (const prop of checker.getPropertiesOfType(member)) {
475
+ const entry = byName.get(prop.name) ?? {symbols: [], optional: 0};
476
+ entry.symbols.push(prop);
477
+ if (prop.flags & ts.SymbolFlags.Optional) entry.optional++;
478
+ byName.set(prop.name, entry);
479
+ }
480
+ }
481
+ if (fillsAll) for (const name of byName.keys()) defaulted.add(name);
482
+
483
+ /** @type {((prop: any) => string) | undefined} */
484
+ let origin;
485
+ // Read from the component's declaration, its props parameter's, and the
486
+ // wrapped component's for `memo` and `lazy`.
487
+ const originOf = (/** @type {any} */ prop) =>
488
+ (origin ??= mappedPropOrigin(ts, checker, isOwnFile, [
489
+ symbol.valueDeclaration ?? symbol.declarations?.[0],
490
+ ...[
491
+ ...signatures,
492
+ ...(holder === type ? [] : holder.getCallSignatures()),
493
+ ].map(
494
+ (/** @type {any} */ signature) =>
495
+ signature.getParameters()[0]?.valueDeclaration,
496
+ ),
497
+ ]))(prop);
498
+ for (const [name, {symbols, optional}] of byName) {
499
+ const declarations = symbols.flatMap(
500
+ /** @param {any} s */ s => s.declarations ?? [],
501
+ );
502
+ const ownDeclarations = declarations.filter(
503
+ /** @param {any} d */ d => isOwnFile(d.getSourceFile().fileName),
504
+ );
505
+ // A prop no declaration names, one a mapped type builds, is the
506
+ // package's own when its key comes from the package's own source.
507
+ const built =
508
+ declarations.length === 0 ? strongest(symbols.map(originOf)) : null;
509
+ // Optional when a caller may leave it out: an overload that takes no
510
+ // props, or a member of the props type that does not require it.
511
+ const required =
512
+ !defaulted.has(name) &&
513
+ !propless &&
514
+ symbols.length === members.length &&
515
+ optional === 0;
516
+ props.set(name, {
517
+ name,
518
+ required,
519
+ own: ownDeclarations.length > 0 || built === 'own',
520
+ untraced:
521
+ declarations.length === 0 && built !== 'own' && built !== 'foreign',
522
+ skipTagged: ownDeclarations.some(
523
+ /** @param {any} d */
524
+ d =>
525
+ ts
526
+ .getJSDocTags(d)
527
+ .some(
528
+ /** @param {any} tag */ tag => SKIP_TAGS.has(tag.tagName.text),
529
+ ),
530
+ ),
531
+ // A property's `type` is the prop's type; a method's is its return
532
+ // type, so a method prop reads as the compiler renders it. A
533
+ // declaration's text counts only for its own symbol, or where a mapping
534
+ // copies its type unchanged (`Pick`, `Omit`): one that rewrites it
535
+ // (`{[K in keyof T]: T[K][]}`, `Required<T>`) is judged by its type.
536
+ declared: symbols.flatMap(
537
+ /** @param {any} s */ s =>
538
+ (s.declarations ?? [])
539
+ .filter(
540
+ /** @param {any} d */ d =>
541
+ ownDeclarations.includes(d) &&
542
+ d.type &&
543
+ !ts.isMethodSignature(d) &&
544
+ !ts.isMethodDeclaration(d) &&
545
+ copiesType(ts, checker, s, d, sourceFile),
546
+ )
547
+ .map(/** @param {any} d */ d => d.type.getText(d.getSourceFile())),
548
+ ),
549
+ types: () =>
550
+ symbols.map(
551
+ /** @param {any} s */ s =>
552
+ checker.getTypeOfSymbolAtLocation(s, sourceFile),
553
+ ),
554
+ intersected: symbols.some(
555
+ /** @param {any} s */ s =>
556
+ (s.declarations ?? []).filter(/** @param {any} d */ d => d.type)
557
+ .length > 1,
558
+ ),
559
+ indexed: memo(() => {
560
+ const lacking = members.filter(
561
+ /** @param {any} m */ m => !checker.getPropertyOfType(m, name),
562
+ );
563
+ const each = lacking.map(
564
+ /** @param {any} m */ m => indexesFor(ts, checker, [m], name),
565
+ );
566
+ return each.some(indexes => indexes.length > 1)
567
+ ? null
568
+ : each.flatMap(indexes => indexes.map(info => info.type));
569
+ }),
570
+ location: ownDeclarations[0] ?? sourceFile,
571
+ // Printed where the prop is declared, where a documented type's names
572
+ // resolve: a type another file imports under another name prints as
573
+ // that file names it.
574
+ renderings: memo(() => {
575
+ const at = ownDeclarations[0] ?? sourceFile;
576
+ /** @type {string[]} */
577
+ const printed = [];
578
+ /** @type {string[]} */
579
+ const expanded = [];
580
+ for (const s of symbols) {
581
+ const propType = checker.getTypeOfSymbolAtLocation(s, sourceFile);
582
+ for (const part of inDeclaredOrder(ts, checker, propType, s)) {
583
+ printed.push(checker.typeToString(part, at, flags));
584
+ expanded.push(
585
+ checker.typeToString(
586
+ part,
587
+ at,
588
+ flags | ts.TypeFormatFlags.InTypeAlias,
589
+ ),
590
+ );
591
+ }
592
+ }
593
+ return [printed, expanded];
594
+ }),
595
+ });
596
+ }
597
+
598
+ /** The index signatures of the props type that take this name. */
599
+ const accepts = (/** @type {string} */ name) =>
600
+ indexesFor(ts, checker, members, name);
601
+ return {ok: true, props, accepts, defaults: literalDefaults(ts, symbol)};
602
+ }
603
+
604
+ /**
605
+ * The defaults a component writes in its props parameter's destructuring
606
+ * (`{size = 'md'}`), by prop name, each as its value: a string, number,
607
+ * boolean, or null, or NOT_LITERAL for any other default (a constant, a call,
608
+ * an object), which is listed as not compared.
609
+ * @param {any} ts
610
+ * @param {any} symbol the component
611
+ * @returns {Map<string, string | number | boolean | null | symbol>}
612
+ */
613
+ function literalDefaults(ts, symbol) {
614
+ /** @type {Map<string, string | number | boolean | null | symbol>} */
615
+ const out = new Map();
616
+ /** @param {any} node @param {number} depth @returns {any} */
617
+ const fnOf = (node, depth) => {
618
+ if (!node || depth > 4) return null;
619
+ if (
620
+ ts.isFunctionDeclaration(node) ||
621
+ ts.isArrowFunction(node) ||
622
+ ts.isFunctionExpression(node)
623
+ ) {
624
+ return node;
625
+ }
626
+ if (ts.isVariableDeclaration(node))
627
+ return fnOf(node.initializer, depth + 1);
628
+ if (
629
+ ts.isParenthesizedExpression(node) ||
630
+ ts.isAsExpression(node) ||
631
+ (ts.isSatisfiesExpression && ts.isSatisfiesExpression(node))
632
+ ) {
633
+ return fnOf(node.expression, depth + 1);
634
+ }
635
+ // `forwardRef(function …)`, `memo(…)`: the function they wrap.
636
+ if (ts.isCallExpression(node)) {
637
+ for (const argument of node.arguments) {
638
+ const found = fnOf(argument, depth + 1);
639
+ if (found) return found;
640
+ }
641
+ }
642
+ return null;
643
+ };
644
+ const fn = fnOf(symbol?.valueDeclaration, 0);
645
+ const pattern = fn?.parameters?.[0]?.name;
646
+ if (!pattern || !ts.isObjectBindingPattern(pattern)) return out;
647
+ for (const element of pattern.elements) {
648
+ if (element.dotDotDotToken || !element.initializer) continue;
649
+ const key = element.propertyName ?? element.name;
650
+ if (!(
651
+ ts.isIdentifier(key) ||
652
+ ts.isStringLiteral(key) ||
653
+ ts.isNumericLiteral(key)
654
+ )) {
655
+ continue;
656
+ }
657
+ const value = literalValue(ts, element.initializer);
658
+ out.set(key.text, value === undefined ? NOT_LITERAL : value);
659
+ }
660
+ return out;
661
+ }
662
+
663
+ /**
664
+ * A literal expression's value, or undefined for anything else.
665
+ * @param {any} ts
666
+ * @param {any} node
667
+ * @returns {string | number | boolean | null | undefined}
668
+ */
669
+ function literalValue(ts, node) {
670
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
671
+ return node.text;
672
+ }
673
+ if (ts.isNumericLiteral(node)) return Number(node.text);
674
+ if (
675
+ ts.isPrefixUnaryExpression(node) &&
676
+ node.operator === ts.SyntaxKind.MinusToken &&
677
+ ts.isNumericLiteral(node.operand)
678
+ ) {
679
+ return -Number(node.operand.text);
680
+ }
681
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
682
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
683
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
684
+ return undefined;
685
+ }
686
+
687
+ /**
688
+ * A documented default as a literal value: a quoted string (quotes of any
689
+ * kind, whitespace around it ignored), a number, `true`, `false`, or
690
+ * `null`; undefined for prose or an expression, which is never compared.
691
+ * @param {unknown} written
692
+ * @returns {string | number | boolean | null | undefined}
693
+ */
694
+ function documentedDefault(written) {
695
+ if (typeof written === 'number' || typeof written === 'boolean') {
696
+ return written;
697
+ }
698
+ if (written === null) return null;
699
+ if (typeof written !== 'string') return undefined;
700
+ const text = written.trim();
701
+ const quoted = /^(['"`])([^'"`]*)\1$/.exec(text);
702
+ if (quoted) return quoted[2];
703
+ if (/^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(text)) return Number(text);
704
+ if (text === 'true') return true;
705
+ if (text === 'false') return false;
706
+ if (text === 'null') return null;
707
+ return undefined;
708
+ }
709
+
710
+ /** @param {string | number | boolean | null} value */
711
+ function defaultText(value) {
712
+ return typeof value === 'string' ? `'${value}'` : String(value);
713
+ }
714
+
715
+ /**
716
+ * A prop type's union members in the order the source declares them,
717
+ * through the type aliases it names; members it doesn't reach, such as the
718
+ * `undefined` an optional prop adds, follow, sorted by their text. The
719
+ * compiler's own order follows when each type was first made, so it would
720
+ * move with whatever read the types first.
721
+ * @param {any} ts
722
+ * @param {any} checker
723
+ * @param {any} type
724
+ * @param {any} symbol the prop
725
+ * @returns {any[]}
726
+ */
727
+ function inDeclaredOrder(ts, checker, type, symbol) {
728
+ if (!type.isUnion()) return [type];
729
+ /** @type {any[]} */
730
+ const order = [];
731
+ const seen = new Set();
732
+ /** @type {(node: any) => void} */
733
+ const visit = node => {
734
+ if (ts.isParenthesizedTypeNode(node)) return visit(node.type);
735
+ if (ts.isUnionTypeNode(node)) return node.types.forEach(visit);
736
+ if (ts.isTypeReferenceNode(node) && !node.typeArguments) {
737
+ let target = checker.getSymbolAtLocation(node.typeName);
738
+ if (target && target.flags & ts.SymbolFlags.Alias) {
739
+ target = checker.getAliasedSymbol(target);
740
+ }
741
+ const alias = target?.declarations?.find(ts.isTypeAliasDeclaration);
742
+ if (alias && !alias.typeParameters) {
743
+ if (!seen.has(alias)) {
744
+ seen.add(alias);
745
+ visit(alias.type);
746
+ }
747
+ return;
748
+ }
749
+ }
750
+ const found = checker.getTypeFromTypeNode(node);
751
+ order.push(...(found.isUnion() ? found.types : [found]));
752
+ };
753
+ for (const declaration of symbol.declarations ?? []) {
754
+ if (declaration.type) visit(declaration.type);
755
+ }
756
+ /** @type {(t: any) => number} */
757
+ const place = t => {
758
+ const at = order.indexOf(t);
759
+ return at === -1 ? order.length : at;
760
+ };
761
+ /** @type {(t: any) => string} */
762
+ const text = t => checker.typeToString(t);
763
+ return [...type.types].sort(
764
+ (a, b) => place(a) - place(b) || text(a).localeCompare(text(b)),
765
+ );
766
+ }
767
+
768
+ /**
769
+ * Whether a declaration's text reads as the prop's type: it is the prop's own
770
+ * declaration, or the generic type's prop it instantiates when the result
771
+ * prints as the declared type in the component's file (a generic component
772
+ * passing its own type parameters on), or a mapping copies its type
773
+ * unchanged. Instantiated with another type (`Box<string>`, `ListProps<T[]>`,
774
+ * or an imported `Item` the file names another way), its text names a type
775
+ * the prop no longer has.
776
+ * @param {any} ts
777
+ * @param {any} checker
778
+ * @param {any} symbol the prop
779
+ * @param {any} declaration one of its declarations
780
+ * @param {any} location
781
+ */
782
+ function copiesType(ts, checker, symbol, declaration, location) {
783
+ const own = declaration.name && checker.getSymbolAtLocation(declaration.name);
784
+ if (!own) return false;
785
+ if (own === symbol) return true;
786
+ const type = checker.getTypeOfSymbolAtLocation(symbol, location);
787
+ const declared = checker.getTypeOfSymbolAtLocation(own, location);
788
+ /** @param {any} t */
789
+ const print = t =>
790
+ checker.typeToString(t, location, ts.TypeFormatFlags.NoTruncation);
791
+ return (
792
+ declared === type ||
793
+ (own === (symbol.links ?? symbol).target && print(type) === print(declared))
794
+ );
795
+ }
796
+
797
+ /**
798
+ * Does the code take `undefined` itself, written or through an alias? With
799
+ * `exactOptionalPropertyTypes` that is part of the type a message shows;
800
+ * otherwise an optional prop takes `undefined` anyway.
801
+ * @param {any} checker
802
+ * @param {CodeProp} code
803
+ * @param {boolean} exactOptional
804
+ */
805
+ function ownUndefined(checker, code, exactOptional) {
806
+ const undefinedType = checker.getUndefinedType?.();
807
+ return (
808
+ exactOptional &&
809
+ (code.declared.some(text => /\bundefined\b/.test(text)) ||
810
+ code
811
+ .types()
812
+ .some(t => (t.isUnion() ? t.types : [t]).includes(undefinedType)))
813
+ );
814
+ }
815
+
816
+ /**
817
+ * One of a prop's renderings for a message, without the `undefined` an
818
+ * optional prop adds, unless the code takes it itself.
819
+ * @param {CodeProp} code
820
+ * @param {number} which 0 as printed, 1 expanded
821
+ * @param {boolean} keepUndefined
822
+ */
823
+ function rendered(code, which, keepUndefined) {
824
+ const parts = code.renderings()[which];
825
+ return [
826
+ ...new Set(
827
+ parts.filter(
828
+ part =>
829
+ !(code.required !== true && part === 'undefined' && !keepUndefined),
830
+ ),
831
+ ),
832
+ ].join(' | ');
833
+ }
834
+
835
+ /**
836
+ * The prop's type for a message: as the source writes it when its
837
+ * declarations agree, else (a method or accessor, or declarations that differ
838
+ * or narrow each other in an intersection) as the compiler prints it.
839
+ * @param {CodeProp} code
840
+ * @param {boolean} keepUndefined
841
+ */
842
+ function codeTypeText(code, keepUndefined) {
843
+ const written = [...new Set(code.declared)];
844
+ const text =
845
+ written.length === 1 ? written[0] : rendered(code, 0, keepUndefined);
846
+ return text.replace(/\s+/g, ' ').trim();
847
+ }
848
+
849
+ /**
850
+ * Judge a documented type against a prop's code type (type-match.mjs).
851
+ * @param {any} ts
852
+ * @param {any} checker
853
+ * @param {string} docType
854
+ * @param {CodeProp} code
855
+ * @param {{nullable: boolean, exactOptional: boolean, isDefaultLibrary: (file: any) => boolean}} reading
856
+ * how the project checks null, and which files are TypeScript's own
857
+ */
858
+ function judgeProp(ts, checker, docType, code, reading) {
859
+ // A member that lacks the prop may take its name through an index
860
+ // signature, which a doc may describe too.
861
+ const indexed = code.indexed();
862
+ if (indexed == null) return 'not_compared';
863
+ return judgeMembers(docType, [...code.types(), ...indexed], {
864
+ ts,
865
+ checker,
866
+ location: code.location,
867
+ optional: code.required !== true && !reading.exactOptional,
868
+ ...reading,
869
+ // Declared text reads as the type only when each declaration is one
870
+ // member's; intersected declarations narrow each other.
871
+ written: code.intersected
872
+ ? code.renderings()
873
+ : [
874
+ code.declared,
875
+ ...code.declared.map(text => [text]),
876
+ ...code.renderings(),
877
+ ],
878
+ });
879
+ }
880
+
881
+ /**
882
+ * Judge a documented type against the types a prop has in the members of a
883
+ * union of props types: a doc may describe one member, so any member's type,
884
+ * or their union, matches, and it is a mismatch only against every one.
885
+ * @param {string} docType
886
+ * @param {any[]} types the type in each member
887
+ * @param {import('./type-match.mjs').JudgeContext} ctx
888
+ * @returns {import('./type-match.mjs').TypeVerdict}
889
+ */
890
+ function judgeMembers(docType, types, ctx) {
891
+ const candidates =
892
+ types.length > 1 ? [types, ...types.map(type => [type])] : [types];
893
+ const verdicts = candidates.map(candidate =>
894
+ judgeType(docType, candidate, ctx),
895
+ );
896
+ if (verdicts.includes('match')) return 'match';
897
+ return verdicts.every(verdict => verdict === 'mismatch')
898
+ ? 'mismatch'
899
+ : 'not_compared';
900
+ }
901
+
902
+ /**
903
+ * The props one doc entry names. `a / b` documents each of `a` and `b`, with
904
+ * the entry's `required` (and the type split the same way when it splits
905
+ * into as many parts); `x.y` (or
906
+ * `x[].y`) documents a field of prop `x`; `...rest` documents a spread of
907
+ * another props type and is not compared.
908
+ * @param {any} raw
909
+ * @returns {Array<{name: string, written: string, path: string[], spread: boolean, type?: string, required?: boolean}>}
910
+ */
911
+ function documentedProps(raw) {
912
+ const names = String(raw.name)
913
+ .split(/\s+\/\s+/)
914
+ .map(name => name.trim())
915
+ .filter(Boolean);
916
+ const types = typeof raw.type === 'string' ? raw.type.split(/\s+\/\s+/) : [];
917
+ return names.map((written, i) => {
918
+ const spread = written.startsWith('...');
919
+ const [root = written, ...path] = written
920
+ .replace(/\[\]/g, '')
921
+ .split('.')
922
+ .filter(Boolean);
923
+ return {
924
+ name: spread ? written : root,
925
+ written,
926
+ path: spread ? [] : path,
927
+ spread,
928
+ type:
929
+ names.length === 1
930
+ ? raw.type
931
+ : types.length === names.length
932
+ ? types[i]
933
+ : undefined,
934
+ required: raw.required,
935
+ };
936
+ });
937
+ }
938
+
939
+ /**
940
+ * The types a field can sit on: each member of a union, through arrays and
941
+ * tuples at any depth (`grid[][].id`).
942
+ * @param {any} ts
943
+ * @param {any} checker
944
+ * @param {any} type
945
+ * @param {number} [depth]
946
+ * @returns {any[]}
947
+ */
948
+ function holders(ts, checker, type, depth = 0) {
949
+ if (depth > 8) return [type];
950
+ if (type.isUnion()) {
951
+ return type.types.flatMap(
952
+ /** @param {any} t */ t => holders(ts, checker, t, depth + 1),
953
+ );
954
+ }
955
+ const array =
956
+ ['Array', 'ReadonlyArray'].includes(type.symbol?.name) ||
957
+ type.target?.objectFlags & ts.ObjectFlags.Tuple;
958
+ if (array && type.objectFlags & ts.ObjectFlags.Reference) {
959
+ return checker
960
+ .getTypeArguments(type)
961
+ .flatMap(/** @param {any} t */ t => holders(ts, checker, t, depth + 1));
962
+ }
963
+ return [type];
964
+ }
965
+
966
+ /**
967
+ * Whether a type takes `undefined` (an index signature's value type, which
968
+ * TypeScript holds an explicit `undefined` to).
969
+ * @param {any} ts
970
+ * @param {any} type
971
+ */
972
+ function takesUndefined(ts, type) {
973
+ return (type.isUnion() ? type.types : [type]).some(
974
+ /** @param {any} t */ t => t.flags & ts.TypeFlags.Undefined,
975
+ );
976
+ }
977
+
978
+ /**
979
+ * The types a field path reaches in a prop's type (in each member of a union
980
+ * that has it, through arrays at any depth), and whether its last field is
981
+ * optional: null when no member has the path, `any` when a type on the way
982
+ * is `any` or `unknown`, which holds any field.
983
+ * @param {any} ts
984
+ * @param {any} checker
985
+ * @param {CodeProp} code
986
+ * @param {string[]} fieldPath
987
+ * @returns {{types: any[], optional: boolean, ambiguous: boolean, indexed: boolean} | 'any' | null}
988
+ * `ambiguous` when two index signatures take a field on the path, and
989
+ * `indexed` when only index signatures take the last field
990
+ */
991
+ function fieldTypes(ts, checker, code, fieldPath) {
992
+ /** @type {any[]} */
993
+ let current = code.types();
994
+ let optional = false;
995
+ let ambiguous = false;
996
+ let indexed = false;
997
+ for (const field of fieldPath) {
998
+ /** @type {any[]} */
999
+ const next = [];
1000
+ optional = false;
1001
+ let byName = false;
1002
+ for (const holder of current.flatMap(t => holders(ts, checker, t))) {
1003
+ if (holder.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
1004
+ return 'any';
1005
+ }
1006
+ const prop = checker.getPropertyOfType(holder, field);
1007
+ if (prop) {
1008
+ next.push(checker.getTypeOfSymbol(prop));
1009
+ if (prop.flags & ts.SymbolFlags.Optional) optional = true;
1010
+ byName = true;
1011
+ continue;
1012
+ }
1013
+ // A field an index signature takes (`Record<string, string>`) is of the
1014
+ // index's type, which takes `undefined` only when it says so; two that
1015
+ // take it hold it to both.
1016
+ const indexes = indexesFor(ts, checker, [holder], field);
1017
+ if (indexes.length > 1) ambiguous = true;
1018
+ if (indexes.length > 0) {
1019
+ next.push(indexes[0].type);
1020
+ if (takesUndefined(ts, indexes[0].type)) optional = true;
1021
+ }
1022
+ }
1023
+ if (next.length === 0) return null;
1024
+ current = next;
1025
+ indexed = !byName;
1026
+ }
1027
+ return {types: current, optional, ambiguous, indexed};
1028
+ }
1029
+
1030
+ /**
1031
+ * The types a field path reaches, printed where the prop is declared, where
1032
+ * a documented type's names resolve.
1033
+ * @param {any} ts
1034
+ * @param {any} checker
1035
+ * @param {{types: any[], optional: boolean}} reached
1036
+ * @param {boolean} exactOptional the project sets `exactOptionalPropertyTypes`
1037
+ * @param {any} location where the prop is declared
1038
+ * @returns {string[]}
1039
+ */
1040
+ function fieldTypeText(ts, checker, reached, exactOptional, location) {
1041
+ // An optional field's `undefined` is the compiler's own; with
1042
+ // `exactOptionalPropertyTypes` it is a marker of its own, and one the code
1043
+ // declares stays.
1044
+ const own = /** @param {any} t */ t =>
1045
+ reached.optional &&
1046
+ t.flags & ts.TypeFlags.Undefined &&
1047
+ (!exactOptional || t !== checker.getUndefinedType?.());
1048
+ return [
1049
+ ...new Set(
1050
+ reached.types
1051
+ .flatMap(t => (t.isUnion() ? t.types : [t]))
1052
+ .filter(t => !own(t))
1053
+ .map(t =>
1054
+ checker.typeToString(
1055
+ t,
1056
+ location,
1057
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
1058
+ ),
1059
+ ),
1060
+ ),
1061
+ ];
1062
+ }
1063
+
1064
+ /**
1065
+ * Is a type the compiler's stand-in for a name it could not resolve?
1066
+ * @param {any} ts
1067
+ * @param {any} type
1068
+ */
1069
+ function isErrorType(ts, type) {
1070
+ return (
1071
+ (type.flags & ts.TypeFlags.Any) !== 0 && type.intrinsicName === 'error'
1072
+ );
1073
+ }
1074
+
1075
+ /**
1076
+ * Do some of a generic component's prop names come from a type parameter:
1077
+ * a deferred type, an index signature keyed or valued by a type made from
1078
+ * one, or a mapped type whose keys are one (`Partial<P>`) or whose `as`
1079
+ * clause uses one? An interface whose values alone use it (`ListProps<T>`),
1080
+ * a mapping over keys of its own (`Record<'value', V>`), and an index that
1081
+ * takes any name (`[key: string]: any`) give names of its own.
1082
+ * @param {any} ts
1083
+ * @param {any} checker
1084
+ * @param {any} type
1085
+ * @param {number} depth
1086
+ * @returns {boolean}
1087
+ */
1088
+ function namesFromTypeParameter(ts, checker, type, depth) {
1089
+ if (depth > 8) return false;
1090
+ if (type.isUnion() || type.isIntersection()) {
1091
+ return type.types.some(
1092
+ /** @param {any} t */ t =>
1093
+ namesFromTypeParameter(ts, checker, t, depth + 1),
1094
+ );
1095
+ }
1096
+ if (isDeferred(ts, checker, type)) return true;
1097
+ if (
1098
+ (checker.getIndexInfosOfType?.(type) ?? []).some(
1099
+ /** @param {any} info */ info =>
1100
+ isDeferred(ts, checker, info.type, true) ||
1101
+ isDeferred(ts, checker, info.keyType, true),
1102
+ )
1103
+ ) {
1104
+ return true;
1105
+ }
1106
+ if ((type.objectFlags & ts.ObjectFlags.Mapped) === 0) return false;
1107
+ // The keys, which the compiler works out when it reads the members.
1108
+ checker.getPropertiesOfType(type);
1109
+ return (
1110
+ (!!type.constraintType && isDeferred(ts, checker, type.constraintType)) ||
1111
+ renamedByTypeParameter(ts, checker, type)
1112
+ );
1113
+ }
1114
+
1115
+ /**
1116
+ * Does a mapping's `as` clause use a type parameter besides its own key that
1117
+ * is still open where the props are read: one the component declares, or one
1118
+ * an alias passes on whose argument is still a type parameter (`` as
1119
+ * `${N}${K}` ``, `as T extends true ? K : never`)? Given fixed arguments
1120
+ * (`Prefixed<'my'>`, a key filter such as `Except<Props, 'b'>`), every key
1121
+ * gets its name. Its own key, and an `infer` inside the clause, are known
1122
+ * for each key.
1123
+ * @param {any} ts
1124
+ * @param {any} checker
1125
+ * @param {any} type a mapped type
1126
+ * @returns {boolean}
1127
+ */
1128
+ function renamedByTypeParameter(ts, checker, type) {
1129
+ const clause = type.declaration?.nameType;
1130
+ if (!clause) return false;
1131
+ /** @type {(node: any) => boolean} */
1132
+ const uses = node => {
1133
+ const symbol = ts.isTypeReferenceNode(node)
1134
+ ? checker.getSymbolAtLocation(node.typeName)
1135
+ : undefined;
1136
+ const declaration =
1137
+ symbol && symbol.flags & ts.SymbolFlags.TypeParameter
1138
+ ? symbol.declarations?.[0]
1139
+ : undefined;
1140
+ const inClause =
1141
+ declaration?.getSourceFile() === clause.getSourceFile() &&
1142
+ declaration.pos >= clause.pos &&
1143
+ declaration.end <= clause.end;
1144
+ if (
1145
+ declaration &&
1146
+ declaration !== type.declaration.typeParameter &&
1147
+ !inClause
1148
+ ) {
1149
+ // Its value where the props are read, through the instantiation.
1150
+ const value = valueIn(
1151
+ ts,
1152
+ checker,
1153
+ checker.getDeclaredTypeOfSymbol(symbol),
1154
+ type.mapper,
1155
+ 0,
1156
+ );
1157
+ if (!value || isDeferred(ts, checker, value, true)) return true;
1158
+ }
1159
+ return !!ts.forEachChild(
1160
+ node,
1161
+ /** @param {any} child */ child => uses(child) || undefined,
1162
+ );
1163
+ };
1164
+ return uses(clause);
1165
+ }
1166
+
1167
+ /**
1168
+ * The value an instantiation gives a type parameter, as the compiler's own
1169
+ * mappers give it, through each mapper one combines; null when that takes
1170
+ * more than following parameters (a value made from one, or a mapper of
1171
+ * another form), which counts as open.
1172
+ * @param {any} ts
1173
+ * @param {any} checker
1174
+ * @param {any} parameter
1175
+ * @param {any} mapper
1176
+ * @param {number} depth
1177
+ * @returns {any}
1178
+ */
1179
+ function valueIn(ts, checker, parameter, mapper, depth) {
1180
+ if (!mapper) return parameter;
1181
+ if (depth > 32) return null;
1182
+ if (mapper.mapper1 && mapper.mapper2) {
1183
+ const first = valueIn(ts, checker, parameter, mapper.mapper1, depth + 1);
1184
+ if (!first) return null;
1185
+ if (first === parameter || first.flags & ts.TypeFlags.TypeParameter) {
1186
+ return valueIn(ts, checker, first, mapper.mapper2, depth + 1);
1187
+ }
1188
+ return isDeferred(ts, checker, first, true) ? null : first;
1189
+ }
1190
+ if ('source' in mapper) {
1191
+ return mapper.source === parameter ? mapper.target : parameter;
1192
+ }
1193
+ if (Array.isArray(mapper.sources)) {
1194
+ const i = mapper.sources.indexOf(parameter);
1195
+ if (i < 0) return parameter;
1196
+ const target = mapper.targets?.[i];
1197
+ return typeof target === 'function' ? target() : (target ?? null);
1198
+ }
1199
+ return typeof mapper.func === 'function' ? mapper.func(parameter) : null;
1200
+ }
1201
+
1202
+ /**
1203
+ * A type parameter, or a type the compiler defers until one is known (an
1204
+ * indexed access, a conditional), or a union, intersection or template
1205
+ * literal with one (`on${K}`, `Capitalize<K>`); with `deep`, also a type made
1206
+ * from one (through alias or reference arguments).
1207
+ * @param {any} ts
1208
+ * @param {any} checker
1209
+ * @param {any} t
1210
+ * @param {boolean} [deep]
1211
+ * @param {number} [depth]
1212
+ * @returns {boolean}
1213
+ */
1214
+ function isDeferred(ts, checker, t, deep = false, depth = 0) {
1215
+ const F = ts.TypeFlags;
1216
+ if (
1217
+ t.flags &
1218
+ (F.TypeParameter |
1219
+ F.IndexedAccess |
1220
+ F.Conditional |
1221
+ F.Substitution |
1222
+ F.Index)
1223
+ ) {
1224
+ return true;
1225
+ }
1226
+ if (depth > 6) return false;
1227
+ const parts = [
1228
+ ...(t.isUnion() || t.isIntersection() ? t.types : []),
1229
+ ...(t.flags & F.TemplateLiteral ? (t.types ?? []) : []),
1230
+ ...(t.flags & F.StringMapping && t.type ? [t.type] : []),
1231
+ ...(deep ? (t.aliasTypeArguments ?? []) : []),
1232
+ ...(deep && t.objectFlags & ts.ObjectFlags.Reference
1233
+ ? checker.getTypeArguments(t)
1234
+ : []),
1235
+ ];
1236
+ return parts.some(u => isDeferred(ts, checker, u, deep, depth + 1));
1237
+ }
1238
+
1239
+ /**
1240
+ * Does an index signature's key take this prop name, as TypeScript would? A
1241
+ * `string` key takes any name; a template literal key only the names that
1242
+ * match it (`data-${string}`, `col${number}`, `on${Capitalize<string>}`).
1243
+ * @param {any} ts
1244
+ * @param {any} checker
1245
+ * @param {any} key
1246
+ * @param {string} name
1247
+ */
1248
+ function keyAccepts(ts, checker, key, name) {
1249
+ if (key.flags & ts.TypeFlags.String) return true;
1250
+ if (!(key.flags & ts.TypeFlags.TemplateLiteral)) return false;
1251
+ if (checker.getStringLiteralType && checker.isTypeAssignableTo) {
1252
+ return checker.isTypeAssignableTo(checker.getStringLiteralType(name), key);
1253
+ }
1254
+ // An older TypeScript has no literal-type factory: match the template.
1255
+ /** @type {Record<string, string>} */
1256
+ const mappings = {
1257
+ Uppercase: '[^a-z]*',
1258
+ Lowercase: '[^A-Z]*',
1259
+ Capitalize: '(?:[^a-z][\\s\\S]*)?',
1260
+ Uncapitalize: '(?:[^A-Z][\\s\\S]*)?',
1261
+ };
1262
+ const spans = (key.types ?? []).map(
1263
+ /** @param {any} span */ span =>
1264
+ span.flags & ts.TypeFlags.Number
1265
+ ? '[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?'
1266
+ : (mappings[String(span.symbol?.escapedName)] ?? '[\\s\\S]*'),
1267
+ );
1268
+ const pattern = (key.texts ?? [])
1269
+ .map(
1270
+ (/** @type {string} */ text, /** @type {number} */ i) =>
1271
+ text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + (spans[i] ?? ''),
1272
+ )
1273
+ .join('');
1274
+ return new RegExp(`^${pattern}$`).test(name);
1275
+ }
1276
+
1277
+ /**
1278
+ * The index signatures of these types that take a name, as TypeScript
1279
+ * matches keys. TypeScript holds a value to every one of them.
1280
+ * @param {any} ts
1281
+ * @param {any} checker
1282
+ * @param {any[]} types
1283
+ * @param {string} name
1284
+ * @returns {any[]}
1285
+ */
1286
+ function indexesFor(ts, checker, types, name) {
1287
+ // One signature seen through several union members is one signature.
1288
+ const seen = new Set();
1289
+ return types
1290
+ .flatMap(type => checker.getIndexInfosOfType?.(type) ?? [])
1291
+ .filter(
1292
+ /** @param {any} info */ info => {
1293
+ const key = `${checker.typeToString(info.keyType)}\0${checker.typeToString(info.type)}`;
1294
+ if (seen.has(key)) return false;
1295
+ seen.add(key);
1296
+ return (
1297
+ !holdsNothing(ts, info.type) &&
1298
+ keyAccepts(ts, checker, info.keyType, name)
1299
+ );
1300
+ },
1301
+ );
1302
+ }
1303
+
1304
+ /**
1305
+ * Can a value of this type be passed at all? `never`, or only `undefined`
1306
+ * (`Record<string, never>`, the usual way to write "no props"), cannot.
1307
+ * @param {any} ts
1308
+ * @param {any} type
1309
+ */
1310
+ function holdsNothing(ts, type) {
1311
+ const parts = type.isUnion() ? type.types : [type];
1312
+ return parts.every(
1313
+ /** @param {any} t */ t =>
1314
+ t.flags & (ts.TypeFlags.Never | ts.TypeFlags.Undefined),
1315
+ );
1316
+ }
1317
+
1318
+ /** @param {Array<string | null>} found the answers for one key, by place */
1319
+ const strongest = found =>
1320
+ ['own', 'foreign', 'unknown'].find(answer => found.includes(answer)) ?? null;
1321
+
1322
+ /**
1323
+ * Where the key of a prop a mapped type builds, which no declaration names,
1324
+ * comes from: `own` when the package's own source writes it, as a literal or
1325
+ * a property's declaration, `foreign` when another package's types do, and
1326
+ * `unknown` when the check can't trace it. It reads the mapping where the
1327
+ * component's declaration and props parameter reach it (and the component a
1328
+ * `memo`, `lazy` or `forwardRef` call wraps), through aliases, a generic's
1329
+ * arguments, `keyof`, `typeof`, and a conditional's branches (`Exclude`,
1330
+ * `Extract`).
1331
+ * @param {any} ts
1332
+ * @param {any} checker
1333
+ * @param {(file: string) => boolean} isOwnFile
1334
+ * @param {any[]} starts
1335
+ * @returns {(prop: any) => string}
1336
+ */
1337
+ function mappedPropOrigin(ts, checker, isOwnFile, starts) {
1338
+ /** @type {Map<any, Map<any, any>[]>} each mapping, with its type arguments */
1339
+ const sites = new Map();
1340
+ let budget = 5000;
1341
+ /** @param {any} node */
1342
+ const where = node =>
1343
+ isOwnFile(node.getSourceFile().fileName) ? 'own' : 'foreign';
1344
+ /** @param {any} node */
1345
+ const target = node => {
1346
+ let symbol = checker.getSymbolAtLocation(
1347
+ node.typeName ?? node.exprName ?? node.expression ?? node,
1348
+ );
1349
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
1350
+ symbol = checker.getAliasedSymbol(symbol);
1351
+ }
1352
+ return symbol;
1353
+ };
1354
+ /** @type {(params: any, args: any, env: Map<any, any>) => Map<any, any>} */
1355
+ const bind = (params, args, env) => {
1356
+ const next = new Map(env);
1357
+ (params ?? []).forEach(
1358
+ (/** @type {any} */ param, /** @type {number} */ i) => {
1359
+ const node = args?.[i] ?? param.default;
1360
+ if (node)
1361
+ next.set(target(param.name), {node, env: args?.[i] ? env : next});
1362
+ },
1363
+ );
1364
+ return next;
1365
+ };
1366
+ /** @type {(node: any, env: Map<any, any>, depth: number) => void} */
1367
+ const visit = (node, env, depth) => {
1368
+ if (!node || depth > 40 || budget-- <= 0) return;
1369
+ if (ts.isMappedTypeNode(node)) {
1370
+ sites.set(node, [...(sites.get(node) ?? []), env]);
1371
+ }
1372
+ const reference =
1373
+ ts.isTypeReferenceNode(node) || ts.isExpressionWithTypeArguments(node);
1374
+ // A component another binding wraps: `memo(Inner)`, `forwardRef(Impl)`.
1375
+ const wrapped =
1376
+ ts.isIdentifier(node) &&
1377
+ (ts.isCallExpression(node.parent) ||
1378
+ ts.isVariableDeclaration(node.parent)) &&
1379
+ node !== node.parent.expression &&
1380
+ node !== node.parent.name;
1381
+ for (const declaration of reference || wrapped
1382
+ ? (target(node)?.declarations ?? [])
1383
+ : []) {
1384
+ const own = where(declaration) === 'own';
1385
+ if (reference && ts.isTypeAliasDeclaration(declaration)) {
1386
+ visit(
1387
+ declaration.type,
1388
+ bind(declaration.typeParameters, node.typeArguments, env),
1389
+ depth + 1,
1390
+ );
1391
+ } else if (reference && (own || !node.typeArguments)) {
1392
+ // Another package's interface: only what it extends.
1393
+ const next = bind(declaration.typeParameters, node.typeArguments, env);
1394
+ if (own) visit(declaration, next, depth + 1);
1395
+ else
1396
+ for (const clause of declaration.heritageClauses ?? [])
1397
+ visit(clause, next, depth + 1);
1398
+ } else if (wrapped && own) {
1399
+ visit(declaration, env, depth + 1);
1400
+ }
1401
+ }
1402
+ ts.forEachChild(node, (/** @type {any} */ child) =>
1403
+ visit(child, env, depth + 1),
1404
+ );
1405
+ };
1406
+ /** @type {(node: any, key: string, env: Map<any, any>, depth: number) => string | null} */
1407
+ const keyOrigin = (node, key, env, depth) => {
1408
+ if (!node || depth > 40) return 'unknown';
1409
+ const next = (/** @type {any} */ inner) =>
1410
+ keyOrigin(inner, key, env, depth + 1);
1411
+ if (ts.isParenthesizedTypeNode(node)) return next(node.type);
1412
+ if (ts.isLiteralTypeNode(node)) {
1413
+ return node.literal.text === key ? where(node) : null;
1414
+ }
1415
+ if (ts.isUnionTypeNode(node)) return strongest(node.types.map(next));
1416
+ if (ts.isConditionalTypeNode(node)) {
1417
+ return strongest([next(node.trueType), next(node.falseType)]);
1418
+ }
1419
+ if (
1420
+ ts.isTypeOperatorNode(node) &&
1421
+ node.operator === ts.SyntaxKind.KeyOfKeyword
1422
+ ) {
1423
+ const of =
1424
+ ts.isTypeReferenceNode(node.type) && env.get(target(node.type));
1425
+ const prop = checker.getPropertyOfType(
1426
+ checker.getTypeFromTypeNode(of ? of.node : node.type),
1427
+ key,
1428
+ );
1429
+ return prop ? propOrigin(prop, depth + 1) : null;
1430
+ }
1431
+ if (ts.isArrayTypeNode(node)) return next(node.elementType);
1432
+ if (ts.isTupleTypeNode(node)) return strongest(node.elements.map(next));
1433
+ if (ts.isTypeOperatorNode(node)) return next(node.type);
1434
+ if (ts.isIndexedAccessTypeNode(node)) {
1435
+ // `Props['size']` reads the property's own type; `(typeof SIZES)[number]`
1436
+ // reads the value's.
1437
+ const index = ts.isLiteralTypeNode(node.indexType)
1438
+ ? node.indexType.literal.text
1439
+ : undefined;
1440
+ if (index === undefined) return next(node.objectType);
1441
+ const declared = checker.getPropertyOfType(
1442
+ checker.getTypeFromTypeNode(node.objectType),
1443
+ index,
1444
+ )?.declarations?.[0];
1445
+ return declared ? valueOrigin(declared, key, depth + 1) : 'unknown';
1446
+ }
1447
+ if (ts.isTypeQueryNode(node)) {
1448
+ return valueOrigin(target(node)?.valueDeclaration, key, depth + 1);
1449
+ }
1450
+ if (ts.isTypeReferenceNode(node)) {
1451
+ const symbol = target(node);
1452
+ const bound = symbol && env.get(symbol);
1453
+ if (bound) return keyOrigin(bound.node, key, bound.env, depth + 1);
1454
+ const alias = symbol?.declarations?.find(ts.isTypeAliasDeclaration);
1455
+ if (alias) {
1456
+ return keyOrigin(
1457
+ alias.type,
1458
+ key,
1459
+ bind(alias.typeParameters, node.typeArguments, env),
1460
+ depth + 1,
1461
+ );
1462
+ }
1463
+ // An enum: where it is declared. A type parameter no argument binds,
1464
+ // such as an `infer`, can't be traced.
1465
+ if (
1466
+ symbol &&
1467
+ symbol.flags & (ts.SymbolFlags.Enum | ts.SymbolFlags.EnumMember)
1468
+ ) {
1469
+ return where(symbol.declarations[0]);
1470
+ }
1471
+ }
1472
+ return 'unknown';
1473
+ };
1474
+ /**
1475
+ * Where a value's literal keys are written: its declared type, or the
1476
+ * literals its initializer writes, through `as const` and other bindings.
1477
+ * @type {(node: any, key: string, depth: number) => string | null}
1478
+ */
1479
+ const valueOrigin = (node, key, depth) => {
1480
+ if (!node || depth > 40) return 'unknown';
1481
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) {
1482
+ return node.text === key ? where(node) : null;
1483
+ }
1484
+ if (ts.isArrayLiteralExpression(node)) {
1485
+ return strongest(
1486
+ node.elements.map((/** @type {any} */ e) =>
1487
+ valueOrigin(e, key, depth + 1),
1488
+ ),
1489
+ );
1490
+ }
1491
+ // A declared type or a cast, but not `as const` or `satisfies`.
1492
+ const written =
1493
+ node.type &&
1494
+ !ts.isSatisfiesExpression?.(node) &&
1495
+ node.type.typeName?.text !== 'const';
1496
+ if (written) return keyOrigin(node.type, key, new Map(), depth + 1);
1497
+ const inner = node.initializer ?? node.expression;
1498
+ if (inner) return valueOrigin(inner, key, depth + 1);
1499
+ if (ts.isIdentifier(node)) {
1500
+ let symbol = checker.getSymbolAtLocation(node);
1501
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
1502
+ symbol = checker.getAliasedSymbol(symbol);
1503
+ }
1504
+ return valueOrigin(symbol?.valueDeclaration, key, depth + 1);
1505
+ }
1506
+ return 'unknown';
1507
+ };
1508
+ /** @type {(prop: any, depth: number) => string | null} */
1509
+ const propOrigin = (prop, depth) => {
1510
+ if (depth > 40) return 'unknown';
1511
+ const declarations = prop.declarations ?? [];
1512
+ if (declarations.length > 0) {
1513
+ return declarations.some((/** @type {any} */ d) => where(d) === 'own')
1514
+ ? 'own'
1515
+ : 'foreign';
1516
+ }
1517
+ const links = prop.links ?? prop;
1518
+ if (links.syntheticOrigin)
1519
+ return propOrigin(links.syntheticOrigin, depth + 1);
1520
+ if (links.containingType) {
1521
+ return strongest(
1522
+ links.containingType.types.map((/** @type {any} */ t) => {
1523
+ const part = checker.getPropertyOfType(t, prop.name);
1524
+ return part && part !== prop ? propOrigin(part, depth + 1) : null;
1525
+ }),
1526
+ );
1527
+ }
1528
+ const mapping = links.mappedType?.declaration;
1529
+ const key = links.keyType?.value;
1530
+ if (!mapping || key === undefined) return 'unknown';
1531
+ return strongest(
1532
+ (sites.get(mapping) ?? []).map(env =>
1533
+ keyOrigin(
1534
+ mapping.typeParameter.constraint,
1535
+ String(key),
1536
+ env,
1537
+ depth + 1,
1538
+ ),
1539
+ ),
1540
+ );
1541
+ };
1542
+ for (const start of starts) {
1543
+ if (start && where(start) === 'own') visit(start, new Map(), 0);
1544
+ }
1545
+ return prop => propOrigin(prop, 0) ?? 'unknown';
1546
+ }
1547
+
1548
+ /**
1549
+ * Are props `any`: a plain `any` (not one standing for a name that does not
1550
+ * resolve), or a type an alias makes from `any` by reading its keys, which
1551
+ * takes every name (`Omit<any, 'ref'>`, or React's `Omit` of one)? `Record<string, any>` only gives
1552
+ * names an `any` value, as an inline `[key: string]: any` does.
1553
+ * @param {any} ts
1554
+ * @param {any} checker
1555
+ * @param {any} type
1556
+ * @param {number} [depth] how far into an alias's arguments
1557
+ * @returns {boolean}
1558
+ */
1559
+ function isAnyProps(ts, checker, type, depth = 0) {
1560
+ if (type.flags & ts.TypeFlags.Any) return !isErrorType(ts, type);
1561
+ const parts = type.isUnion() || type.isIntersection() ? type.types : [type];
1562
+ /** @type {(node: any) => any} the symbol a reference names */
1563
+ const target = node => {
1564
+ const used = checker.getSymbolAtLocation(node.typeName ?? node.expression);
1565
+ return used && used.flags & ts.SymbolFlags.Alias
1566
+ ? checker.getAliasedSymbol(used)
1567
+ : used;
1568
+ };
1569
+ /** @type {(node: any) => any} */
1570
+ const aliasOf = node =>
1571
+ target(node)?.declarations?.find(ts.isTypeAliasDeclaration);
1572
+ // Whether an alias reads its `i`th parameter's keys: `keyof T`, or `T`
1573
+ // passed on to an alias that does, as `type Without<T> = Omit<T, 'as'>`.
1574
+ /** @type {(alias: any, i: number, depth?: number) => boolean} */
1575
+ const readsKeys = (alias, i, depth = 0) => {
1576
+ const name = alias?.typeParameters?.[i]?.name.text;
1577
+ /** @type {(node: any) => boolean} */
1578
+ const visit = node =>
1579
+ (ts.isTypeOperatorNode(node) &&
1580
+ node.operator === ts.SyntaxKind.KeyOfKeyword &&
1581
+ node.type.typeName?.text === name) ||
1582
+ (ts.isTypeReferenceNode(node) &&
1583
+ depth < 8 &&
1584
+ (node.typeArguments ?? []).some(
1585
+ (/** @type {any} */ arg, /** @type {number} */ j) =>
1586
+ arg.typeName?.text === name &&
1587
+ readsKeys(aliasOf(node), j, depth + 1),
1588
+ )) ||
1589
+ !!ts.forEachChild(
1590
+ node,
1591
+ (/** @type {any} */ child) => visit(child) || undefined,
1592
+ );
1593
+ return !!name && visit(alias);
1594
+ };
1595
+ /** @type {(symbol: any) => any[]} an alias's type, or an interface's bases */
1596
+ const shapeOf = symbol =>
1597
+ (symbol?.declarations ?? []).flatMap((/** @type {any} */ d) =>
1598
+ ts.isTypeAliasDeclaration(d)
1599
+ ? [d.type]
1600
+ : (d.heritageClauses?.flatMap((/** @type {any} */ c) => c.types) ?? []),
1601
+ );
1602
+ // `any`, or such a type, passed where an alias reads its keys, written in
1603
+ // the props type or one of the package's names for it (`type P =
1604
+ // Without<any>`, `interface P extends Omit<Omit<any, 'as'>, 'ref'>`).
1605
+ /** @type {(node: any, depth: number) => boolean} */
1606
+ const anyKeyed = (node, depth) => {
1607
+ if (!node || depth > 8) return false;
1608
+ if (node.kind === ts.SyntaxKind.AnyKeyword) return true;
1609
+ if (ts.isParenthesizedTypeNode(node)) return anyKeyed(node.type, depth + 1);
1610
+ if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
1611
+ return node.types.some((/** @type {any} */ t) => anyKeyed(t, depth + 1));
1612
+ }
1613
+ // A reference: `Name<…>`, or an interface's `extends Name<…>`.
1614
+ if (!node.typeName && !node.expression) return false;
1615
+ const alias = aliasOf(node);
1616
+ return node.typeArguments
1617
+ ? node.typeArguments.some(
1618
+ (/** @type {any} */ arg, /** @type {number} */ i) =>
1619
+ anyKeyed(arg, depth + 1) && readsKeys(alias, i),
1620
+ )
1621
+ : shapeOf(target(node)).some(n => anyKeyed(n, depth + 1));
1622
+ };
1623
+ return parts.some(
1624
+ /** @param {any} part */ part =>
1625
+ ((part.aliasTypeArguments ?? []).some(
1626
+ /** @type {(t: any, i: number) => boolean} */ (t, i) =>
1627
+ (t.flags & ts.TypeFlags.Any
1628
+ ? !isErrorType(ts, t)
1629
+ : depth < 4 && isAnyProps(ts, checker, t, depth + 1)) &&
1630
+ readsKeys(part.aliasSymbol?.declarations?.[0], i),
1631
+ ) ||
1632
+ shapeOf(part.aliasSymbol ?? part.symbol).some(n => anyKeyed(n, 0))) &&
1633
+ (checker.getIndexInfosOfType?.(part) ?? []).some(
1634
+ /** @param {any} info */ info => info.type.flags & ts.TypeFlags.Any,
1635
+ ),
1636
+ );
1637
+ }
1638
+
1639
+ /**
1640
+ * Is any part of a props type `any`, or built from a type the compiler could
1641
+ * not resolve (`Omit<Missing, 'x'>`, `Partial<Missing>`)? Then props it would
1642
+ * inherit are missing or unknown, and comparing would report documented
1643
+ * props as absent, or pass them unread.
1644
+ * @param {any} ts
1645
+ * @param {any} checker
1646
+ * @param {any} type
1647
+ * @param {number} depth
1648
+ * @param {Set<any>} seen types already read, so shared bases are read once
1649
+ * @returns {boolean}
1650
+ */
1651
+ function hasUnresolvedPart(ts, checker, type, depth, seen) {
1652
+ if (depth > 24 || seen.has(type)) return false;
1653
+ seen.add(type);
1654
+ if (type.flags & ts.TypeFlags.Any) return true;
1655
+ // A type made from another keeps what it was made from: an alias its
1656
+ // arguments, a generic reference its type arguments.
1657
+ const made = [
1658
+ ...(type.aliasTypeArguments ?? []),
1659
+ ...(type.objectFlags & ts.ObjectFlags.Reference
1660
+ ? checker.getTypeArguments(type)
1661
+ : []),
1662
+ ];
1663
+ if (made.some(/** @param {any} t */ t => isErrorType(ts, t))) return true;
1664
+ // A mapped type over one that does not resolve keeps no arguments behind a
1665
+ // named alias (`type Base = Pick<Missing, 'title'>`): its properties have
1666
+ // the compiler's error type.
1667
+ if (
1668
+ type.objectFlags & ts.ObjectFlags.Mapped &&
1669
+ checker
1670
+ .getPropertiesOfType(type)
1671
+ .some(
1672
+ /** @param {any} p */ p => isErrorType(ts, checker.getTypeOfSymbol(p)),
1673
+ )
1674
+ ) {
1675
+ return true;
1676
+ }
1677
+ // A mapped type written over one that does not resolve
1678
+ // (`{[K in keyof Missing]?: string}`) keeps it only in its declaration.
1679
+ const constraint = type.declaration?.typeParameter?.constraint;
1680
+ const mappedOver =
1681
+ constraint && ts.isTypeOperatorNode(constraint)
1682
+ ? constraint.type
1683
+ : constraint;
1684
+ if (
1685
+ type.objectFlags & ts.ObjectFlags.Mapped &&
1686
+ mappedOver &&
1687
+ isErrorType(ts, checker.getTypeFromTypeNode(mappedOver))
1688
+ ) {
1689
+ return true;
1690
+ }
1691
+ // A type mapped over one that does not resolve (`Omit<Missing, 'x'>`,
1692
+ // `Partial<Missing>`), reached directly, through a named alias, or nested,
1693
+ // reads as index signatures whose value is that unresolved type, which no
1694
+ // source declares.
1695
+ if (
1696
+ type.flags & ts.TypeFlags.Object &&
1697
+ (checker.getIndexInfosOfType?.(type) ?? []).some(
1698
+ /** @param {any} info */ info =>
1699
+ !info.declaration && isErrorType(ts, info.type),
1700
+ )
1701
+ ) {
1702
+ return true;
1703
+ }
1704
+ if (type.isUnion() || type.isIntersection()) {
1705
+ return type.types.some(
1706
+ /** @param {any} t */ t =>
1707
+ hasUnresolvedPart(ts, checker, t, depth + 1, seen),
1708
+ );
1709
+ }
1710
+ if (type.flags & ts.TypeFlags.Object) {
1711
+ const target =
1712
+ type.objectFlags & ts.ObjectFlags.Reference ? type.target : type;
1713
+ if (target.objectFlags & ts.ObjectFlags.ClassOrInterface) {
1714
+ // An unresolved `extends` is dropped from the base types, so read the
1715
+ // heritage clauses themselves.
1716
+ for (const declaration of target.symbol?.declarations ?? []) {
1717
+ for (const clause of declaration.heritageClauses ?? []) {
1718
+ for (const node of clause.types) {
1719
+ const base = checker.getTypeAtLocation(node);
1720
+ if (hasUnresolvedPart(ts, checker, base, depth + 1, seen))
1721
+ return true;
1722
+ }
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ return false;
1728
+ }
1729
+
1730
+ /**
1731
+ * Compare each documented prop table with the TypeScript props of the export
1732
+ * it describes.
1733
+ * @param {PropDriftTable[]} tables
1734
+ * @param {{packageDir: string, resolveFrom: Array<string | null | undefined>}} options
1735
+ * @returns {PropDriftReport}
1736
+ */
1737
+ export function comparePropTables(tables, options) {
1738
+ const {packageDir, resolveFrom} = options;
1739
+ const compiler = resolveProjectTypeScript(resolveFrom);
1740
+ if (!compiler?.ts) {
1741
+ return {
1742
+ status: 'skipped',
1743
+ reason: compiler
1744
+ ? unusableTypeScriptReason(
1745
+ compiler.version,
1746
+ 'documented props were not compared with the code',
1747
+ )
1748
+ : TYPESCRIPT_MISSING_REASON,
1749
+ components: 0,
1750
+ typesCompared: 0,
1751
+ findings: [],
1752
+ unchecked: [],
1753
+ typesNotCompared: [],
1754
+ };
1755
+ }
1756
+ const {ts, version} = compiler;
1757
+ let root = path.resolve(packageDir);
1758
+ try {
1759
+ root = fs.realpathSync(root);
1760
+ } catch {
1761
+ // Keep the resolved path.
1762
+ }
1763
+ const rel = (/** @type {string} */ file) =>
1764
+ path.relative(packageDir, file).split(path.sep).join('/');
1765
+
1766
+ /** @type {PropDriftUnchecked[]} */
1767
+ const unchecked = [];
1768
+ if (tables.length === 0) {
1769
+ return {
1770
+ status: 'checked',
1771
+ typescript: version,
1772
+ components: 0,
1773
+ typesCompared: 0,
1774
+ findings: [],
1775
+ unchecked,
1776
+ typesNotCompared: [],
1777
+ };
1778
+ }
1779
+
1780
+ const project = readPackageTsconfig(
1781
+ ts,
1782
+ packageDir,
1783
+ tables.flatMap(table => table.sources),
1784
+ );
1785
+ if (project.error) {
1786
+ // Read without the package's own config, the types would be guesses.
1787
+ for (const table of tables) {
1788
+ unchecked.push({
1789
+ component: table.name,
1790
+ doc: table.doc,
1791
+ reason: `the package's TypeScript config has an error, so props were not read (${project.error})`,
1792
+ });
1793
+ }
1794
+ unchecked.sort((a, b) => a.component.localeCompare(b.component));
1795
+ return {
1796
+ status: 'checked',
1797
+ typescript: version,
1798
+ components: 0,
1799
+ typesCompared: 0,
1800
+ findings: [],
1801
+ unchecked,
1802
+ typesNotCompared: [],
1803
+ };
1804
+ }
1805
+ const program = ts.createProgram({
1806
+ rootNames: [
1807
+ ...new Set([
1808
+ ...tables.flatMap(table => table.sources),
1809
+ ...project.declarations,
1810
+ ]),
1811
+ ],
1812
+ options: project.options,
1813
+ });
1814
+ const checker = program.getTypeChecker();
1815
+ const isOwnFile = ownFileTest(root);
1816
+ const ambient = unresolvedGlobals(
1817
+ ts,
1818
+ program,
1819
+ checker,
1820
+ project.declarations,
1821
+ project.options,
1822
+ isOwnFile,
1823
+ );
1824
+ // Without strict null checks the compiler drops `null` and `undefined`.
1825
+ // Whether they are on by default depends on the TypeScript version.
1826
+ const compilerOptions = program.getCompilerOptions();
1827
+ const nullable = !(typeof ts.getStrictOptionValue === 'function'
1828
+ ? ts.getStrictOptionValue(compilerOptions, 'strictNullChecks')
1829
+ : (compilerOptions.strictNullChecks ?? compilerOptions.strict ?? false));
1830
+ const reading = {
1831
+ nullable,
1832
+ // An optional prop takes `undefined` only when the project lets it.
1833
+ exactOptional:
1834
+ !nullable && compilerOptions.exactOptionalPropertyTypes === true,
1835
+ isDefaultLibrary: (/** @type {any} */ file) =>
1836
+ program.isSourceFileDefaultLibrary(file),
1837
+ };
1838
+
1839
+ /** @type {PropDriftFinding[]} */
1840
+ const findings = [];
1841
+ /** @type {Array<PropDriftUnchecked & {props: string[]}>} */
1842
+ const propsNotChecked = [];
1843
+ let compared = 0;
1844
+ let typesCompared = 0;
1845
+ /** @type {PropDriftTypesNotCompared[]} */
1846
+ const typesNotCompared = [];
1847
+ const typesSkipped = typeTextTooOld(ts);
1848
+ for (const table of tables) {
1849
+ /** @type {ReturnType<typeof readCodeProps> | null} */
1850
+ let read = null;
1851
+ let sourcePath = table.sources[0];
1852
+ for (const candidate of table.sources) {
1853
+ const attempt = readCodeProps(
1854
+ ts,
1855
+ program,
1856
+ checker,
1857
+ candidate,
1858
+ table.name,
1859
+ isOwnFile,
1860
+ ambient,
1861
+ );
1862
+ if (attempt.ok || read == null) {
1863
+ read = attempt;
1864
+ sourcePath = candidate;
1865
+ }
1866
+ if (attempt.ok) break;
1867
+ }
1868
+ if (read == null || !read.ok) {
1869
+ unchecked.push({
1870
+ component: table.name,
1871
+ doc: table.doc,
1872
+ reason:
1873
+ read != null && !read.ok
1874
+ ? read.reason
1875
+ : `no source file for ${table.name}`,
1876
+ });
1877
+ continue;
1878
+ }
1879
+ compared++;
1880
+ const source = rel(sourcePath);
1881
+ const documented = new Set();
1882
+ /** @type {Map<string, Set<string>>} */
1883
+ const notCompared = new Map();
1884
+ /** @type {(reason: string, name: string) => void} */
1885
+ const skip = (reason, name) => {
1886
+ notCompared.set(reason, (notCompared.get(reason) ?? new Set()).add(name));
1887
+ };
1888
+ for (const raw of table.props) {
1889
+ if (!raw || typeof raw.name !== 'string') continue;
1890
+ const entries = documentedProps(raw);
1891
+ for (const entry of entries) {
1892
+ const propName = entry.name;
1893
+ if (entry.spread) continue;
1894
+ const docType = entry.type;
1895
+ if (typesSkipped) entry.type = undefined;
1896
+ documented.add(propName);
1897
+ const code = read.props.get(propName);
1898
+ const reached =
1899
+ code && entry.path.length > 0
1900
+ ? fieldTypes(ts, checker, code, entry.path)
1901
+ : null;
1902
+ if (
1903
+ entry.type === undefined &&
1904
+ !typesSkipped &&
1905
+ typeof raw.type === 'string' &&
1906
+ (code && entry.path.length > 0
1907
+ ? reached
1908
+ : code || read.accepts(propName).length > 0)
1909
+ ) {
1910
+ // `a / b` with one type that does not split across the names (a
1911
+ // field only once it is found).
1912
+ skip(
1913
+ code && !code.own ? outside(propName, code) : NOT_EXACT,
1914
+ entry.written,
1915
+ );
1916
+ }
1917
+ const base = {
1918
+ component: table.name,
1919
+ prop: entry.written,
1920
+ severity: /** @type {const} */ ('warning'),
1921
+ doc: table.doc,
1922
+ source,
1923
+ };
1924
+ if (entry.path.length > 0) {
1925
+ // Listed only once the field is found; a missing one is reported.
1926
+ if (code && !code.own && reached && typeof entry.type === 'string') {
1927
+ skip(outside(propName, code), entry.written);
1928
+ }
1929
+ if (reached === 'any') {
1930
+ // A field of an `any` or `unknown` prop has no type to judge.
1931
+ if (code?.own && typeof entry.type === 'string') {
1932
+ skip(NOT_EXACT, entry.written);
1933
+ }
1934
+ continue;
1935
+ }
1936
+ if (code && reached) {
1937
+ const fieldText = fieldTypeText(
1938
+ ts,
1939
+ checker,
1940
+ reached,
1941
+ reading.exactOptional,
1942
+ code.location,
1943
+ );
1944
+ const ctx = {
1945
+ ts,
1946
+ checker,
1947
+ location: code.location,
1948
+ // A name an index signature takes sets the index's
1949
+ // `undefined` aside, as a prop of that name does.
1950
+ optional:
1951
+ reached.optional && (!reading.exactOptional || reached.indexed),
1952
+ ...reading,
1953
+ written: [fieldText],
1954
+ };
1955
+ const verdict =
1956
+ code.own && typeof entry.type === 'string'
1957
+ ? reached.ambiguous
1958
+ ? 'not_compared'
1959
+ : judgeType(entry.type, reached.types, ctx)
1960
+ : null;
1961
+ // The field takes every variant's type: the types of only some
1962
+ // variants are neither its type nor wrong. Past six, the sets of
1963
+ // them are not tried, and the type is never reported.
1964
+ const variants = [...new Set(reached.types)];
1965
+ const someVariants =
1966
+ (verdict === 'mismatch' || verdict === 'not_compared') &&
1967
+ !reached.ambiguous &&
1968
+ variants.length > 1 &&
1969
+ (variants.length > 6 ||
1970
+ Array.from({length: 2 ** variants.length - 2}, (_, i) =>
1971
+ variants.filter((__, j) => ((i + 1) >> j) & 1),
1972
+ ).some(
1973
+ some => judgeType(entry.type ?? '', some, ctx) === 'match',
1974
+ ));
1975
+ if (someVariants) skip(VARIANTS, entry.written);
1976
+ else if (verdict === 'not_compared') skip(NOT_EXACT, entry.written);
1977
+ else if (verdict) typesCompared++;
1978
+ if (verdict === 'mismatch' && !someVariants) {
1979
+ const codeType = fieldText.join(' | ');
1980
+ findings.push({
1981
+ ...base,
1982
+ kind: 'type_mismatch',
1983
+ message: `"${entry.written}" is documented as ${entry.type}, but the code declares ${codeType}.`,
1984
+ docType: entry.type,
1985
+ codeType,
1986
+ });
1987
+ }
1988
+ continue;
1989
+ }
1990
+ if (!code && read.accepts(propName).length > 0) {
1991
+ // A field of a name only an index signature takes.
1992
+ if (typeof entry.type === 'string') skip(NOT_EXACT, entry.written);
1993
+ continue;
1994
+ }
1995
+ findings.push({
1996
+ ...base,
1997
+ kind: 'not_in_code',
1998
+ message: code
1999
+ ? `"${entry.written}" is documented, but ${table.name}'s "${propName}" has no "${entry.path.join('.')}".`
2000
+ : `"${entry.written}" is documented, but ${table.name}'s props have no "${propName}".`,
2001
+ });
2002
+ continue;
2003
+ }
2004
+ if (!code) {
2005
+ // A name an index signature takes is a prop of the index's type;
2006
+ // TypeScript holds it to every signature that takes it, so with
2007
+ // several its type is not compared. Another package's signature is
2008
+ // not the package's to judge, and a mapped type's (`Record`) has no
2009
+ // declaration to say whose it is.
2010
+ const indexes = read.accepts(propName);
2011
+ const own = indexes.filter(
2012
+ (/** @type {any} */ i) =>
2013
+ i.declaration &&
2014
+ isOwnFile(i.declaration.getSourceFile().fileName),
2015
+ );
2016
+ const index = indexes.length === 1 ? own[0] : null;
2017
+ if (indexes.length > 0 && !index && typeof entry.type === 'string') {
2018
+ const declared = indexes.every(
2019
+ (/** @type {any} */ i) => i.declaration,
2020
+ );
2021
+ skip(
2022
+ own.length || !declared ? NOT_EXACT : outside(propName),
2023
+ propName,
2024
+ );
2025
+ }
2026
+ if (index && typeof entry.type === 'string') {
2027
+ const verdict = judgeType(entry.type, [index.type], {
2028
+ ts,
2029
+ checker,
2030
+ location: index.declaration,
2031
+ // An index signature takes `undefined` only when its type does.
2032
+ optional: takesUndefined(ts, index.type),
2033
+ ...reading,
2034
+ // Printed where the index is declared, as the doc's names
2035
+ // resolve there.
2036
+ written: [
2037
+ [
2038
+ checker.typeToString(
2039
+ index.type,
2040
+ index.declaration,
2041
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
2042
+ ),
2043
+ ],
2044
+ ],
2045
+ });
2046
+ if (verdict === 'not_compared') skip(NOT_EXACT, propName);
2047
+ else typesCompared++;
2048
+ if (verdict === 'mismatch') {
2049
+ const codeType = checker.typeToString(
2050
+ index.type,
2051
+ index.declaration,
2052
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
2053
+ );
2054
+ findings.push({
2055
+ ...base,
2056
+ kind: 'type_mismatch',
2057
+ message: `"${propName}" is documented as ${entry.type}, but ${table.name}'s props take it as ${codeType}.`,
2058
+ docType: entry.type,
2059
+ codeType,
2060
+ });
2061
+ }
2062
+ }
2063
+ if (indexes.length > 0) continue;
2064
+ findings.push({
2065
+ ...base,
2066
+ kind: 'not_in_code',
2067
+ message: `"${propName}" is documented, but ${table.name}'s props have no "${propName}".`,
2068
+ ...(typeof docType === 'string' ? {docType} : {}),
2069
+ });
2070
+ continue;
2071
+ }
2072
+ const docRequired = entry.required === true;
2073
+ if (docRequired !== code.required) {
2074
+ findings.push({
2075
+ ...base,
2076
+ kind: 'required_mismatch',
2077
+ message: code.required
2078
+ ? `"${propName}" is required in the code, but the doc does not mark it required.`
2079
+ : `"${propName}" is documented as required, but it is optional in the code.`,
2080
+ docRequired,
2081
+ codeRequired: code.required,
2082
+ });
2083
+ }
2084
+ // A literal default the code writes in its props destructuring and a
2085
+ // literal the doc writes must agree; where both write a default and
2086
+ // either is not a literal, the default is listed as not compared.
2087
+ const codeDefault = read.defaults?.get(propName);
2088
+ const docWrites =
2089
+ entries.length === 1 &&
2090
+ raw.default !== undefined &&
2091
+ raw.default !== '';
2092
+ const docDefault = docWrites
2093
+ ? documentedDefault(raw.default)
2094
+ : undefined;
2095
+ if (codeDefault !== undefined && docWrites) {
2096
+ if (codeDefault === NOT_LITERAL) {
2097
+ skip(CODE_DEFAULT_NOT_LITERAL, propName);
2098
+ } else if (docDefault === undefined) {
2099
+ skip(DOC_DEFAULT_NOT_LITERAL, propName);
2100
+ }
2101
+ }
2102
+ if (
2103
+ codeDefault !== undefined &&
2104
+ codeDefault !== NOT_LITERAL &&
2105
+ docDefault !== undefined &&
2106
+ codeDefault !== docDefault
2107
+ ) {
2108
+ const shown = defaultText(
2109
+ /** @type {string | number | boolean | null} */ (codeDefault),
2110
+ );
2111
+ findings.push({
2112
+ ...base,
2113
+ kind: 'default_mismatch',
2114
+ message: `"${propName}" is documented with default ${String(raw.default).trim()}, but the code defaults it to ${shown}.`,
2115
+ fix: `In ${table.doc}, set the "${propName}" prop's default to the code's: default: ${JSON.stringify(shown)}.`,
2116
+ docDefault: String(raw.default),
2117
+ codeDefault: shown,
2118
+ });
2119
+ }
2120
+ if (!code.own && typeof entry.type === 'string') {
2121
+ skip(outside(propName, code), propName);
2122
+ }
2123
+ if (code.own && typeof entry.type === 'string') {
2124
+ const verdict = judgeProp(
2125
+ ts,
2126
+ checker,
2127
+ String(entry.type),
2128
+ code,
2129
+ reading,
2130
+ );
2131
+ if (verdict === 'not_compared') skip(NOT_EXACT, propName);
2132
+ else typesCompared++;
2133
+ if (verdict === 'mismatch') {
2134
+ const keep = ownUndefined(checker, code, reading.exactOptional);
2135
+ const codeType = codeTypeText(code, keep);
2136
+ const expanded = rendered(code, 1, keep);
2137
+ // The optional prop's `undefined` the expansion sets aside is no
2138
+ // difference to show.
2139
+ const bare = codeType
2140
+ .split(' | ')
2141
+ .filter(part => part !== 'undefined')
2142
+ .join(' | ');
2143
+ const shown =
2144
+ expanded && expanded !== codeType && expanded !== bare
2145
+ ? `${codeType} (${expanded.length <= 400 ? expanded : `${expanded.slice(0, 400)}…`})`
2146
+ : codeType;
2147
+ findings.push({
2148
+ ...base,
2149
+ kind: 'type_mismatch',
2150
+ message: `"${propName}" is documented as ${entry.type}, but the code declares ${shown}.`,
2151
+ docType: entry.type,
2152
+ codeType,
2153
+ });
2154
+ }
2155
+ }
2156
+ }
2157
+ }
2158
+ /** @type {string[]} */
2159
+ const untraced = [];
2160
+ for (const code of read.props.values()) {
2161
+ if (
2162
+ documented.has(code.name) ||
2163
+ code.skipTagged ||
2164
+ PROPS_NOT_DOCUMENTED.includes(code.name)
2165
+ ) {
2166
+ continue;
2167
+ }
2168
+ if (!code.own) {
2169
+ if (code.untraced) untraced.push(code.name);
2170
+ continue;
2171
+ }
2172
+ const codeType = codeTypeText(
2173
+ code,
2174
+ ownUndefined(checker, code, reading.exactOptional),
2175
+ );
2176
+ findings.push({
2177
+ component: table.name,
2178
+ prop: code.name,
2179
+ kind: 'not_in_doc',
2180
+ severity: 'warning',
2181
+ message: `${table.name} declares "${code.name}", but the doc does not list it.`,
2182
+ doc: table.doc,
2183
+ source,
2184
+ codeType,
2185
+ codeRequired: code.required,
2186
+ });
2187
+ }
2188
+ if (untraced.length > 0) {
2189
+ propsNotChecked.push({
2190
+ component: table.name,
2191
+ doc: table.doc,
2192
+ reason: `a mapped type builds them from keys the check can't trace to the package's source, so it did not check that the doc lists them`,
2193
+ props: untraced.sort(),
2194
+ });
2195
+ }
2196
+ for (const [reason, props] of notCompared) {
2197
+ typesNotCompared.push({
2198
+ component: table.name,
2199
+ doc: table.doc,
2200
+ reason,
2201
+ props: [...props].sort(),
2202
+ });
2203
+ }
2204
+ }
2205
+
2206
+ // Field by field: collation ignores a separator such as NUL, which would
2207
+ // sort one component's findings among another's that shares its prefix.
2208
+ findings.sort(
2209
+ (a, b) =>
2210
+ a.component.localeCompare(b.component) ||
2211
+ a.prop.localeCompare(b.prop) ||
2212
+ a.kind.localeCompare(b.kind),
2213
+ );
2214
+ unchecked.sort((a, b) => a.component.localeCompare(b.component));
2215
+ typesNotCompared.sort(
2216
+ (a, b) =>
2217
+ a.component.localeCompare(b.component) ||
2218
+ a.reason.localeCompare(b.reason),
2219
+ );
2220
+ return {
2221
+ status: 'checked',
2222
+ typescript: version,
2223
+ components: compared,
2224
+ typesCompared,
2225
+ findings,
2226
+ unchecked,
2227
+ typesNotCompared,
2228
+ ...(typesSkipped ? {typesSkipped} : {}),
2229
+ ...(propsNotChecked.length > 0 ? {propsNotChecked} : {}),
2230
+ };
2231
+ }
2232
+
2233
+ /**
2234
+ * The prop tables one doc file documents, each with the export it describes.
2235
+ * A multi-component doc names each entry's export; an entry without `props`
2236
+ * (a reference to a sibling doc) and a hook entry are not prop tables.
2237
+ * @param {any} doc
2238
+ * @param {PropDriftComponent} component
2239
+ * @returns {Array<{name: string, props: any[]}>}
2240
+ */
2241
+ function propTables(doc, component) {
2242
+ if (!doc || typeof doc !== 'object') return [];
2243
+ /** @type {Array<{name: string, props: any[]}>} */
2244
+ const tables = [];
2245
+ // A doc's own props document the export its `name` names, which can be
2246
+ // another file's export than the one its file is named after.
2247
+ const own = ownName(doc, component);
2248
+ if (Array.isArray(doc.props) && !HOOK_NAME.test(own)) {
2249
+ tables.push({name: own, props: doc.props});
2250
+ }
2251
+ if (Array.isArray(doc.components)) {
2252
+ for (const entry of doc.components) {
2253
+ if (
2254
+ entry &&
2255
+ typeof entry.name === 'string' &&
2256
+ Array.isArray(entry.props) &&
2257
+ !(Array.isArray(doc.props) && entry.name === own) &&
2258
+ !HOOK_NAME.test(entry.name)
2259
+ ) {
2260
+ tables.push({name: entry.name, props: entry.props});
2261
+ }
2262
+ }
2263
+ }
2264
+ return tables;
2265
+ }
2266
+
2267
+ /**
2268
+ * The export a doc's own props document: its `name`, else its record's.
2269
+ * @param {any} doc
2270
+ * @param {PropDriftComponent} component
2271
+ */
2272
+ function ownName(doc, component) {
2273
+ return typeof doc.name === 'string' && doc.name !== ''
2274
+ ? doc.name
2275
+ : component.name;
2276
+ }
2277
+
2278
+ /**
2279
+ * The stem of a `<stem>.doc.{ts,mjs,js}` file: the name of the record that
2280
+ * owns the doc.
2281
+ * @param {string} docPath
2282
+ */
2283
+ function docStem(docPath) {
2284
+ return path.basename(docPath).replace(/\.doc\.(ts|mjs|js)$/, '');
2285
+ }
2286
+
2287
+ /**
2288
+ * Where an entry's export lives: `<entry>.tsx` beside the doc, else the doc's
2289
+ * own same-stem source, else the directory's `index.ts`/`index.tsx`, else the
2290
+ * `.tsx` in the doc's folder that declares it, as discovery finds a member's.
2291
+ * @param {string} name
2292
+ * @param {PropDriftComponent} component
2293
+ * @returns {string[]} candidates, in order
2294
+ */
2295
+ function sourcesForEntry(name, component) {
2296
+ const own = component.sourcePath ? [component.sourcePath] : [];
2297
+ if (name === component.name) return own;
2298
+ const dir = path.dirname(component.docPath);
2299
+ return [
2300
+ path.join(dir, `${name}.tsx`),
2301
+ ...own,
2302
+ path.join(dir, 'index.ts'),
2303
+ path.join(dir, 'index.tsx'),
2304
+ folderSourceFor(dir, name),
2305
+ ].filter(
2306
+ /** @type {(file: string | null, i: number, all: Array<string | null>) => file is string} */ (
2307
+ (file, i, all) =>
2308
+ file != null && all.indexOf(file) === i && fs.existsSync(file)
2309
+ ),
2310
+ );
2311
+ }
2312
+
2313
+ /**
2314
+ * Compare documented props with each component's TypeScript props: find each
2315
+ * doc's prop tables, then compare them (`comparePropTables`).
2316
+ * @param {PropDriftComponent[]} components
2317
+ * @param {{packageDir: string, resolveFrom: Array<string | null | undefined>, loadDoc?: (docPath: string) => Promise<any>}} options
2318
+ * @returns {Promise<PropDriftReport>}
2319
+ */
2320
+ export async function comparePropDocs(components, options) {
2321
+ const {packageDir, resolveFrom, loadDoc = loadComponentDoc} = options;
2322
+ const rel = (/** @type {string} */ file) =>
2323
+ path.relative(packageDir, file).split(path.sep).join('/');
2324
+
2325
+ /** @type {PropDriftTable[]} */
2326
+ const tables = [];
2327
+ /** @type {PropDriftUnchecked[]} */
2328
+ const unchecked = [];
2329
+ const seen = new Set();
2330
+ // A family doc can stand behind several records: its own, named after the
2331
+ // doc, and one per member entry that discovery records as a component of
2332
+ // its own. A member record compares only its own entry, against its own
2333
+ // source; the family record leaves those entries to the members.
2334
+ /** @type {Map<string, Set<string>>} */
2335
+ const namesByDoc = new Map();
2336
+ for (const component of components) {
2337
+ const names = namesByDoc.get(component.docPath) ?? new Set();
2338
+ names.add(component.name);
2339
+ namesByDoc.set(component.docPath, names);
2340
+ }
2341
+ /** @type {Map<string, Promise<any>>} */
2342
+ const docs = new Map();
2343
+ /** @type {Set<string>} docs whose own record is here */
2344
+ const owned = new Set();
2345
+ // Its own props, and every entry no member record compares: a hidden one,
2346
+ // or one discovery doesn't record. One whose source isn't found is listed
2347
+ // as not compared.
2348
+ const compareOwn = (
2349
+ /** @type {PropDriftComponent} */ component,
2350
+ /** @type {any} */ doc,
2351
+ ) => {
2352
+ const members = namesByDoc.get(component.docPath) ?? new Set();
2353
+ for (const table of propTables(doc, component)) {
2354
+ if (table.name !== component.name && members.has(table.name)) continue;
2355
+ const sources = sourcesForEntry(table.name, component);
2356
+ // One table per doc: two docs that each document a component both
2357
+ // get compared.
2358
+ const key = `${component.docPath}\0${sources[0]}\0${table.name}`;
2359
+ if (seen.has(key)) continue;
2360
+ seen.add(key);
2361
+ tables.push({doc: rel(component.docPath), ...table, sources});
2362
+ }
2363
+ };
2364
+ for (const component of components) {
2365
+ let doc;
2366
+ try {
2367
+ let loading = docs.get(component.docPath);
2368
+ if (!loading) {
2369
+ loading = Promise.resolve(loadDoc(component.docPath));
2370
+ docs.set(component.docPath, loading);
2371
+ }
2372
+ doc = await loading;
2373
+ } catch (err) {
2374
+ unchecked.push({
2375
+ component: component.name,
2376
+ doc: rel(component.docPath),
2377
+ reason: `its doc does not load: ${err instanceof Error ? err.message : String(err)}`,
2378
+ });
2379
+ continue;
2380
+ }
2381
+ // The doc's own record is named after its file, or after the export its
2382
+ // `name` names; any other record is a family member's.
2383
+ const ownRecord =
2384
+ component.name === docStem(component.docPath) ||
2385
+ (doc != null &&
2386
+ typeof doc === 'object' &&
2387
+ ownName(doc, component) === component.name);
2388
+ if (!ownRecord) {
2389
+ // A family member compares its own entry.
2390
+ const entry = Array.isArray(doc?.components)
2391
+ ? doc.components.find(
2392
+ (/** @type {any} */ candidate) =>
2393
+ candidate?.name === component.name &&
2394
+ Array.isArray(candidate.props),
2395
+ )
2396
+ : undefined;
2397
+ if (!entry || HOOK_NAME.test(component.name)) continue;
2398
+ const key = `${component.docPath}\0${component.sourcePath}\0${component.name}`;
2399
+ if (seen.has(key)) continue;
2400
+ seen.add(key);
2401
+ tables.push({
2402
+ doc: rel(component.docPath),
2403
+ name: component.name,
2404
+ props: entry.props,
2405
+ sources: component.sourcePath ? [component.sourcePath] : [],
2406
+ });
2407
+ continue;
2408
+ }
2409
+ owned.add(component.docPath);
2410
+ compareOwn(component, doc);
2411
+ }
2412
+ // A family doc whose own record is missing, because its same-stem source
2413
+ // is, still has its own props and unrecorded entries compared or listed.
2414
+ for (const [docPath, loading] of docs) {
2415
+ if (owned.has(docPath)) continue;
2416
+ const doc = await loading.catch(() => null);
2417
+ if (doc == null || typeof doc !== 'object') continue;
2418
+ compareOwn({name: docStem(docPath), docPath, sourcePath: null}, doc);
2419
+ }
2420
+ const report = comparePropTables(tables, {packageDir, resolveFrom});
2421
+ if (report.status === 'skipped') return report;
2422
+ return {
2423
+ ...report,
2424
+ unchecked: [...unchecked, ...report.unchecked].sort((a, b) =>
2425
+ a.component.localeCompare(b.component),
2426
+ ),
2427
+ };
2428
+ }