@astryxdesign/cli 0.7.2 → 0.7.3-canary.02f5fe4

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 (373) hide show
  1. package/README.md +104 -71
  2. package/api/build/_adapter.d.mts +58 -0
  3. package/api/build/_adapter.mjs +127 -3
  4. package/api/build/blocks/blocks.d.mts +92 -0
  5. package/api/build/blocks/blocks.mjs +528 -0
  6. package/api/build/blocks/blocks.test.mjs +238 -0
  7. package/api/build/blocks/blocks.weights.json +1 -0
  8. package/api/build/build.mjs +25 -6
  9. package/api/build/build.type.mjs +1 -1
  10. package/api/build/kit/answer.d.mts +18 -0
  11. package/api/build/kit/answer.mjs +195 -0
  12. package/api/build/kit/answer.test.mjs +108 -0
  13. package/api/build/kit/kind.d.mts +25 -0
  14. package/api/build/kit/kind.mjs +170 -0
  15. package/api/build/kit/kit.d.mts +2 -4
  16. package/api/build/kit/kit.mjs +8 -398
  17. package/api/build/kit/rank.d.mts +3 -32
  18. package/api/build/kit/rank.mjs +25 -342
  19. package/api/build/kit/rank.test.mjs +2 -1
  20. package/api/build/kit/signals.d.mts +79 -0
  21. package/api/build/kit/signals.mjs +295 -0
  22. package/api/build/kit/start.d.mts +32 -0
  23. package/api/build/kit/start.mjs +163 -0
  24. package/api/build/kit/words.d.mts +72 -0
  25. package/api/build/kit/words.mjs +202 -0
  26. package/api/build/tree.d.mts +20 -0
  27. package/api/build/tree.mjs +53 -0
  28. package/api/build/tree.test.mjs +185 -0
  29. package/api/build/tree.weights.json +1 -0
  30. package/api/component/_adapter.mjs +39 -5
  31. package/api/component/component-replacement.test.mjs +1 -0
  32. package/api/component/component.mjs +17 -3
  33. package/api/component/component.test.mjs +15 -0
  34. package/api/component/doc-links.test.mjs +2 -2
  35. package/api/discover/_adapter.d.mts +1 -2
  36. package/api/discover/_adapter.mjs +42 -17
  37. package/api/discover/_adapter.test.mjs +42 -0
  38. package/api/discover/discover.mjs +0 -1
  39. package/api/docs/_adapter.d.mts +2 -2
  40. package/api/docs/_adapter.mjs +82 -23
  41. package/api/docs/docs-homes.test.mjs +127 -0
  42. package/api/docs/docs.doc.mjs +3 -3
  43. package/api/docs/docs.test.mjs +35 -19
  44. package/api/docs/integration-tree.test.mjs +20 -20
  45. package/api/docs/integrationDocs.test.mjs +1 -1
  46. package/api/docs/namespace-section.test.mjs +6 -6
  47. package/api/docs/reference-blocks.test.mjs +2 -2
  48. package/api/doctor/_adapter.d.mts +33 -13
  49. package/api/doctor/_adapter.mjs +55 -25
  50. package/api/doctor/doctor.d.mts +23 -5
  51. package/api/doctor/doctor.mjs +183 -40
  52. package/api/doctor/doctor.test.mjs +95 -12
  53. package/api/hook/hook.mjs +1 -1
  54. package/api/init/init.doc.mjs +20 -6
  55. package/api/init/init.test.mjs +18 -2
  56. package/api/init/init.type.d.mts +3 -3
  57. package/api/init/init.type.mjs +3 -3
  58. package/api/init/remove/remove.mjs +17 -4
  59. package/api/init/remove/remove.test.mjs +114 -2
  60. package/api/init/run/run.mjs +69 -6
  61. package/api/integration/_adapter.d.mts +8 -0
  62. package/api/integration/_adapter.mjs +21 -1
  63. package/api/integration/authoring-checks.mjs +83 -9
  64. package/api/integration/authoring-checks.test.mjs +220 -0
  65. package/api/integration/pack-check.mjs +123 -4
  66. package/api/integration/pack-check.test.mjs +87 -0
  67. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  68. package/api/integration/validate-integration-fixes.test.mjs +6 -5
  69. package/api/integration/validate-integration-gaps.test.mjs +1975 -0
  70. package/api/integration/validate-integration.d.mts +15 -2
  71. package/api/integration/validate-integration.mjs +298 -39
  72. package/api/integration/validate-integration.type.d.mts +90 -1
  73. package/api/integration/validate-integration.type.mjs +67 -2
  74. package/api/integration/validateIntegration.doc.mjs +1 -1
  75. package/api/search/ORDERING.md +28 -0
  76. package/api/search/_adapter.d.mts +82 -2
  77. package/api/search/_adapter.mjs +157 -10
  78. package/api/search/_adapter.test.mjs +173 -0
  79. package/api/search/_candidate-score.d.mts +40 -0
  80. package/api/search/_candidate-score.mjs +238 -0
  81. package/api/search/_cards.json +6098 -0
  82. package/api/search/_compound.d.mts +50 -0
  83. package/api/search/_compound.mjs +170 -0
  84. package/api/search/_doc-searcher.d.mts +4 -0
  85. package/api/search/_doc-searcher.mjs +23 -0
  86. package/api/search/_doc-searcher.test.mjs +36 -0
  87. package/api/search/_doc-searcher.weights.json +1 -0
  88. package/api/search/_doc-text.d.mts +30 -0
  89. package/api/search/_doc-text.mjs +87 -0
  90. package/api/search/_entries.d.mts +63 -0
  91. package/api/search/_entries.mjs +232 -0
  92. package/api/search/_entries.test.mjs +48 -0
  93. package/api/search/_features.d.mts +35 -0
  94. package/api/search/_features.mjs +155 -0
  95. package/api/search/_gather-components.d.mts +26 -0
  96. package/api/search/_gather-components.mjs +284 -0
  97. package/api/search/_gather-docs.d.mts +20 -0
  98. package/api/search/_gather-docs.mjs +254 -0
  99. package/api/search/_gather-hooks.d.mts +12 -0
  100. package/api/search/_gather-hooks.mjs +52 -0
  101. package/api/search/_gather-templates.d.mts +25 -0
  102. package/api/search/_gather-templates.mjs +107 -0
  103. package/api/search/_lexical.d.mts +135 -0
  104. package/api/search/_lexical.mjs +351 -0
  105. package/api/search/_main-searcher.d.mts +19 -0
  106. package/api/search/_main-searcher.mjs +115 -0
  107. package/api/search/_main-searcher.test.mjs +106 -0
  108. package/api/search/_main-searcher.weights.json +1 -0
  109. package/api/search/_module-doc.d.mts +32 -0
  110. package/api/search/_module-doc.mjs +78 -0
  111. package/api/search/_order.d.mts +32 -0
  112. package/api/search/_order.mjs +162 -0
  113. package/api/search/_order.test.mjs +72 -0
  114. package/api/search/_priority.d.mts +50 -0
  115. package/api/search/_priority.mjs +139 -0
  116. package/api/search/_query-words.d.mts +25 -0
  117. package/api/search/_query-words.mjs +92 -0
  118. package/api/search/_query.d.mts +65 -0
  119. package/api/search/_query.mjs +176 -0
  120. package/api/search/_result.d.mts +97 -0
  121. package/api/search/_result.mjs +150 -0
  122. package/api/search/_score.d.mts +60 -0
  123. package/api/search/_score.mjs +283 -0
  124. package/api/search/_synonyms.d.mts +21 -0
  125. package/api/search/_synonyms.mjs +67 -0
  126. package/api/search/_template-searcher.d.mts +4 -0
  127. package/api/search/_template-searcher.mjs +23 -0
  128. package/api/search/_template-searcher.test.mjs +40 -0
  129. package/api/search/_template-searcher.weights.json +1 -0
  130. package/api/search/_titles.d.mts +33 -0
  131. package/api/search/_titles.mjs +65 -0
  132. package/api/search/_words.d.mts +64 -0
  133. package/api/search/_words.mjs +157 -0
  134. package/api/search/search.d.mts +22 -305
  135. package/api/search/search.doc.mjs +8 -6
  136. package/api/search/search.mjs +56 -2482
  137. package/api/search/search.test.mjs +14 -14
  138. package/api/search/search.type.d.mts +6 -1
  139. package/api/search/search.type.mjs +6 -1
  140. package/api/search/tree.d.mts +15 -0
  141. package/api/search/tree.mjs +40 -0
  142. package/api/search/tree.test.mjs +66 -0
  143. package/api/search/tree.weights.json +1 -0
  144. package/api/template/template.mjs +5 -1
  145. package/api/theme/build/build.packed-old-core.test.mjs +1 -0
  146. package/api/upgrade/_adapter.d.mts +57 -9
  147. package/api/upgrade/_adapter.mjs +198 -28
  148. package/api/upgrade/registry/registry.d.mts +10 -4
  149. package/api/upgrade/registry/registry.mjs +33 -16
  150. package/api/upgrade/registry/registry.test.mjs +85 -0
  151. package/api/upgrade/run/run.d.mts +4 -2
  152. package/api/upgrade/run/run.mjs +13 -2
  153. package/api/upgrade/run/run.test.mjs +417 -3
  154. package/api/upgrade/upgrade.doc.mjs +10 -5
  155. package/api/upgrade/upgrade.mjs +13 -2
  156. package/api/upgrade/upgrade.test.mjs +194 -0
  157. package/api/upgrade/upgrade.type.d.mts +5 -3
  158. package/api/upgrade/upgrade.type.mjs +8 -6
  159. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  160. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  161. package/assets/docs/author-a-theme.doc.mjs +2 -2
  162. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  163. package/assets/docs/tree/add-a-component.doc.mjs +1 -1
  164. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  165. package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
  166. package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
  167. package/assets/docs/tree/api.doc.mjs +6 -0
  168. package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
  169. package/assets/docs/tree/block-template.doc.mjs +1 -1
  170. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  171. package/assets/docs/tree/building-blocks.doc.mjs +2 -2
  172. package/assets/docs/tree/check-the-props.doc.mjs +1 -1
  173. package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
  174. package/assets/docs/tree/checks.doc.mjs +3 -3
  175. package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
  176. package/assets/docs/tree/cli-command.doc.mjs +1 -1
  177. package/assets/docs/tree/cli.doc.mjs +1 -1
  178. package/assets/docs/tree/codemods.doc.mjs +1 -1
  179. package/assets/docs/tree/component-family.doc.mjs +1 -1
  180. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  181. package/assets/docs/tree/components.doc.mjs +1 -1
  182. package/assets/docs/tree/configuration.doc.mjs +1 -1
  183. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
  184. package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
  185. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  186. package/assets/docs/tree/docs.doc.mjs +1 -1
  187. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  188. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  189. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  190. package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
  191. package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
  192. package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
  193. package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
  194. package/assets/docs/tree/global-integrations.doc.mjs +2 -2
  195. package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
  196. package/assets/docs/tree/help.doc.mjs +2 -2
  197. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  198. package/assets/docs/tree/links.doc.mjs +1 -1
  199. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  200. package/assets/docs/tree/page-template.doc.mjs +1 -1
  201. package/assets/docs/tree/publishing.doc.mjs +1 -1
  202. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  203. package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
  204. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
  207. package/assets/docs/tree/ship.doc.mjs +2 -2
  208. package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
  209. package/assets/docs/tree/single-component.doc.mjs +1 -1
  210. package/assets/docs/tree/start-a-template.doc.mjs +1 -1
  211. package/assets/docs/tree/subcomponent.doc.mjs +1 -1
  212. package/assets/docs/tree/template-assets.doc.mjs +1 -1
  213. package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
  214. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  215. package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
  216. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  217. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  218. package/assets/docs/tree/template-styles.doc.mjs +1 -1
  219. package/assets/docs/tree/templates.doc.mjs +1 -1
  220. package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
  221. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  222. package/assets/docs/tree/themes.doc.mjs +1 -1
  223. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  224. package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
  225. package/assets/docs/tree/upgrading.doc.mjs +1 -1
  226. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
  227. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  228. package/assets/docs/tree/versioning.doc.mjs +5 -1
  229. package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
  230. package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
  231. package/assets/docs/use-integrations.doc.mjs +120 -0
  232. package/assets/docs/working-with-ai.doc.mjs +6 -2
  233. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  234. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  235. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  236. package/authoring/debug/parse.d.mts +1 -1
  237. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  238. package/authoring/integration/integration.doc.mjs +1 -1
  239. package/authoring/integration/type.ts +1 -1
  240. package/clients/cli/bin/astryx.mjs +7 -78
  241. package/clients/cli/command-result-coverage.test.mjs +6 -5
  242. package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
  243. package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
  244. package/clients/cli/commands/component.test.mjs +6 -6
  245. package/clients/cli/commands/docs.doc.mjs +2 -2
  246. package/clients/cli/commands/docs.mjs +1 -1
  247. package/clients/cli/commands/docs.test.mjs +14 -14
  248. package/clients/cli/commands/doctor-integration.test.mjs +49 -1
  249. package/clients/cli/commands/doctor.doc.mjs +1 -1
  250. package/clients/cli/commands/doctor.mjs +40 -0
  251. package/clients/cli/commands/doctor.test.mjs +65 -0
  252. package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
  253. package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
  254. package/clients/cli/commands/init.behavior.test.mjs +230 -1
  255. package/clients/cli/commands/init.doc.mjs +28 -9
  256. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  257. package/clients/cli/commands/init.mjs +2 -2
  258. package/clients/cli/commands/integration-check.test.mjs +48 -0
  259. package/clients/cli/commands/integration.doc.mjs +1 -1
  260. package/clients/cli/commands/manifest.doc.mjs +1 -1
  261. package/clients/cli/commands/manifest.mjs +67 -0
  262. package/clients/cli/commands/outside-project.test.mjs +101 -1
  263. package/clients/cli/commands/postinstall.doc.mjs +23 -0
  264. package/clients/cli/commands/postinstall.mjs +31 -0
  265. package/clients/cli/commands/setup-nudge.test.mjs +78 -0
  266. package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
  267. package/clients/cli/commands/upgrade.doc.mjs +4 -2
  268. package/clients/cli/commands/upgrade.mjs +9 -2
  269. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  270. package/clients/cli/index.mjs +54 -94
  271. package/clients/cli/lib/component-format.mjs +1 -1
  272. package/clients/cli/lib/define-command.mjs +12 -7
  273. package/clients/cli/lib/exit-codes.test.mjs +5 -0
  274. package/clients/cli/lib/manifest.test.mjs +12 -0
  275. package/clients/cli/lib/welcome-box.mjs +50 -0
  276. package/clients/cli/run.mjs +108 -0
  277. package/foundation/agent-docs/agent-doc-state.d.mts +32 -7
  278. package/foundation/agent-docs/agent-doc-state.mjs +58 -9
  279. package/foundation/agent-docs/agent-docs.d.mts +100 -12
  280. package/foundation/agent-docs/agent-docs.mjs +1017 -105
  281. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  282. package/foundation/agent-docs/agent-docs.test.mjs +683 -39
  283. package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
  284. package/foundation/agent-docs/computer-agent-text.mjs +135 -8
  285. package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
  286. package/foundation/config/project.mjs +20 -22
  287. package/foundation/config/project.test.mjs +56 -0
  288. package/foundation/discovery/authoring-self-docs.d.mts +7 -0
  289. package/foundation/discovery/authoring-self-docs.mjs +16 -4
  290. package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
  291. package/foundation/discovery/authoring-surface.d.mts +1 -1
  292. package/foundation/discovery/authoring-surface.mjs +2 -2
  293. package/foundation/discovery/authoring-surface.test.mjs +1 -1
  294. package/foundation/discovery/cli-self-docs.d.mts +2 -1
  295. package/foundation/discovery/cli-self-docs.mjs +4 -3
  296. package/foundation/discovery/cli-self-docs.test.mjs +2 -2
  297. package/foundation/discovery/component-discovery.d.mts +37 -0
  298. package/foundation/discovery/component-discovery.mjs +80 -5
  299. package/foundation/discovery/component-loader.d.mts +1 -1
  300. package/foundation/discovery/docs-discovery.mjs +14 -1
  301. package/foundation/discovery/module-exports.d.mts +41 -0
  302. package/foundation/discovery/module-exports.mjs +253 -0
  303. package/foundation/discovery/runner-allowlist.d.mts +2 -4
  304. package/foundation/discovery/runner-allowlist.mjs +2 -4
  305. package/foundation/discovery/runner-commands.d.mts +77 -0
  306. package/foundation/discovery/runner-commands.mjs +294 -0
  307. package/foundation/discovery/runner-commands.test.mjs +128 -0
  308. package/foundation/discovery/stray-pointers.test.mjs +1 -1
  309. package/foundation/discovery/template-adapter.mjs +18 -1
  310. package/foundation/discovery/template-needs.mjs +1 -44
  311. package/foundation/discovery/theme-discovery.mjs +147 -3
  312. package/foundation/doc-compiler/doc-loads.test.mjs +33 -9
  313. package/foundation/doc-compiler/read.mjs +59 -1
  314. package/foundation/doc-compiler/read.test.mjs +11 -2
  315. package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
  316. package/foundation/doc-compiler/tree.d.mts +32 -6
  317. package/foundation/doc-compiler/tree.mjs +77 -43
  318. package/foundation/doc-compiler/tree.test.mjs +7 -7
  319. package/foundation/fs/paths.d.mts +70 -8
  320. package/foundation/fs/paths.mjs +556 -38
  321. package/foundation/fs/paths.test.mjs +430 -54
  322. package/foundation/fs/read-record.d.mts +18 -0
  323. package/foundation/fs/read-record.mjs +131 -0
  324. package/foundation/fs/read-record.test.mjs +77 -0
  325. package/foundation/integrations/autolink.d.mts +2 -1
  326. package/foundation/integrations/autolink.mjs +4 -13
  327. package/foundation/integrations/cli-peer-version.d.mts +36 -0
  328. package/foundation/integrations/cli-peer-version.mjs +132 -0
  329. package/foundation/integrations/cli-peer-version.test.mjs +354 -0
  330. package/foundation/integrations/computer-integrations.d.mts +6 -3
  331. package/foundation/integrations/computer-integrations.mjs +38 -9
  332. package/foundation/integrations/contribution-inventory.d.mts +15 -20
  333. package/foundation/integrations/contribution-inventory.mjs +41 -5
  334. package/foundation/integrations/exports-subpaths.d.mts +57 -0
  335. package/foundation/integrations/exports-subpaths.mjs +113 -0
  336. package/foundation/integrations/integrations.d.mts +27 -1
  337. package/foundation/integrations/integrations.mjs +61 -2
  338. package/foundation/integrations/public-imports.d.mts +118 -0
  339. package/foundation/integrations/public-imports.mjs +1769 -0
  340. package/foundation/integrations/public-imports.test.mjs +1793 -0
  341. package/foundation/integrations/validate-contributions.d.mts +115 -0
  342. package/foundation/integrations/validate-contributions.mjs +1168 -13
  343. package/foundation/release/cache.test.mjs +24 -21
  344. package/foundation/release/global-state.d.mts +8 -4
  345. package/foundation/release/global-state.mjs +4 -2
  346. package/foundation/release/handoff.d.mts +23 -4
  347. package/foundation/release/handoff.mjs +175 -8
  348. package/foundation/release/notices.d.mts +62 -8
  349. package/foundation/release/notices.mjs +136 -25
  350. package/foundation/release/notices.test.mjs +21 -0
  351. package/foundation/release/run-context.mjs +14 -0
  352. package/foundation/response/response-types.doc.mjs +1 -1
  353. package/foundation/route/route.d.mts +285 -0
  354. package/foundation/route/route.mjs +324 -0
  355. package/foundation/route/route.test.mjs +153 -0
  356. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  357. package/foundation/runtime/locked-versions.d.mts +32 -0
  358. package/foundation/runtime/locked-versions.mjs +65 -0
  359. package/foundation/runtime/package-root.d.mts +91 -0
  360. package/foundation/runtime/package-root.mjs +174 -0
  361. package/foundation/runtime/package-set.d.mts +62 -0
  362. package/foundation/runtime/package-set.mjs +130 -0
  363. package/foundation/runtime/package-set.test.mjs +432 -0
  364. package/foundation/text/string-utils.d.mts +2 -1
  365. package/foundation/text/string-utils.mjs +16 -5
  366. package/foundation/text/suggest-order.d.mts +72 -0
  367. package/foundation/text/suggest-order.mjs +363 -0
  368. package/foundation/text/suggest-order.test.mjs +259 -0
  369. package/host/door-conformance.test.mjs +274 -0
  370. package/host/index.d.ts +116 -0
  371. package/host/index.mjs +295 -0
  372. package/host/index.test.mjs +302 -0
  373. package/package.json +14 -9
@@ -0,0 +1,1769 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Source-level checks that what an integration package tells apps to
5
+ * import is importable, read without a build.
6
+ *
7
+ * @input A package directory and its loaded integration record.
8
+ * @output Notes, listed on validate's result and never counted (they fail
9
+ * `--strict` from the next minor), for: a component's import (the doc's
10
+ * `import`, or the one the CLI derives) or a template's public import whose
11
+ * subpath package.json `exports` does not declare (or that names another
12
+ * package), or whose module does not export the component; a component
13
+ * example that imports an undeclared subpath or a name its package does not
14
+ * export; a template source with no default export, or one that imports a
15
+ * relative path outside the templates root, an undeclared subpath of its own
16
+ * package or of Core, or a name its package does not export. Imports it
17
+ * cannot judge are listed in a report.
18
+ * @position foundation/integrations, beside the other on-disk contribution
19
+ * validators; run by `astryx doctor integration validate` (and so by
20
+ * `integration check`) through api/integration/validate-integration.mjs.
21
+ *
22
+ * An `exports` target may point at build output that does not exist yet, so
23
+ * only the declared subpath is judged here, never the target file. A name is
24
+ * judged only when a declared target exists on disk and its export list can be
25
+ * read completely, and (for a name other than a doc's component) no
26
+ * TypeScript file in the package has a `declare module` block for one of its
27
+ * own modules (which can add names to it); anything less is left to
28
+ * `integration verify`.
29
+ */
30
+
31
+ import * as fs from 'node:fs';
32
+ import * as path from 'node:path';
33
+ import {parse} from '@babel/parser';
34
+ import {findProjectCoreDir} from '../fs/paths.mjs';
35
+ import {normalizeProviderId} from '../identity/provider-identity.mjs';
36
+ import {
37
+ discoverIntegrationComponents,
38
+ resolveIntegrationImportPath,
39
+ } from '../discovery/component-discovery.mjs';
40
+ import {loadComponentDoc} from '../discovery/component-loader.mjs';
41
+ import {discoverIntegrationTemplatesForOne} from '../discovery/template-adapter.mjs';
42
+ import {
43
+ allTargets,
44
+ importable,
45
+ isRecord,
46
+ subpathDeclaration,
47
+ } from './exports-subpaths.mjs';
48
+
49
+ /**
50
+ * @typedef {import('./issue').AstryxIntegrationIssue} Issue
51
+ */
52
+
53
+ const CORE = '@astryxdesign/core';
54
+ const MODULE_EXTENSIONS = [
55
+ '.tsx',
56
+ '.ts',
57
+ '.mts',
58
+ '.jsx',
59
+ '.mjs',
60
+ '.js',
61
+ '.cjs',
62
+ '.cts',
63
+ ];
64
+
65
+ /** @param {string} file */
66
+ function readJson(file) {
67
+ try {
68
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /** @param {string} file */
75
+ function isFile(file) {
76
+ try {
77
+ return fs.statSync(file).isFile();
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /** @param {string} from @param {string} to */
84
+ function rel(from, to) {
85
+ return path.relative(from, to).split(path.sep).join('/');
86
+ }
87
+
88
+ // ── Import specifiers ───────────────────────────────────────────────
89
+
90
+ /**
91
+ * Split a bare specifier into its package name and `./`-prefixed subpath.
92
+ * @param {string} specifier
93
+ * @returns {{pkg: string, subpath: string} | null}
94
+ */
95
+ export function splitSpecifier(specifier) {
96
+ const m = /^((?:@[^/\s]+\/)?[^/\s]+)(\/.*)?$/.exec(specifier);
97
+ if (!m || specifier.startsWith('.') || specifier.startsWith('/')) return null;
98
+ return {pkg: m[1], subpath: m[2] ? `.${m[2]}` : '.'};
99
+ }
100
+
101
+ /**
102
+ * @typedef {object} ImportBinding
103
+ * @property {string} imported the exported name (`default` for a default import)
104
+ * @property {boolean} typeOnly
105
+ */
106
+
107
+ /**
108
+ * @typedef {object} ImportStatement
109
+ * @property {string} source
110
+ * @property {ImportBinding[]} names
111
+ * @property {boolean} namespace
112
+ * @property {boolean} [typeOnly] the whole statement is `import type`
113
+ */
114
+
115
+ const PARSE_PLUGINS = /** @type {any} */ ([
116
+ 'jsx',
117
+ 'typescript',
118
+ 'decorators-legacy',
119
+ 'importAttributes',
120
+ 'exportDefaultFrom',
121
+ ]);
122
+
123
+ /** The plugins without `jsx`: `.ts` files allow `<T>value` assertions. */
124
+ const TS_PARSE_PLUGINS = PARSE_PLUGINS.filter(
125
+ (/** @type {unknown} */ plugin) => plugin !== 'jsx',
126
+ );
127
+
128
+ /**
129
+ * @param {string} text
130
+ * @param {{jsx?: boolean}} [options] `jsx: false` parses TypeScript only
131
+ */
132
+ function parseModuleText(text, {jsx = true} = {}) {
133
+ return parse(text, {
134
+ sourceType: 'module',
135
+ allowImportExportEverywhere: true,
136
+ allowReturnOutsideFunction: true,
137
+ allowAwaitOutsideFunction: true,
138
+ errorRecovery: true,
139
+ plugins: jsx ? PARSE_PLUGINS : TS_PARSE_PLUGINS,
140
+ });
141
+ }
142
+
143
+ const IMPORT_RE =
144
+ /(?:^|[;\n])\s*import\s+(type\s+)?([^'";]*?)\s*from\s*['"]([^'"]+)['"]/g;
145
+
146
+ /**
147
+ * Read the import statements of a module or a snippet: named, default,
148
+ * aliased, namespace and type-only. A snippet that does not parse (an example
149
+ * with elisions, say) falls back to reading `import … from '…'` lines.
150
+ * @param {string} text
151
+ * @returns {ImportStatement[]}
152
+ */
153
+ export function readImports(text) {
154
+ /** @type {ImportStatement[]} */
155
+ const out = [];
156
+ let ast;
157
+ try {
158
+ ast = parseModuleText(text);
159
+ } catch {
160
+ // Not a module: read the import lines below.
161
+ }
162
+ if (ast) {
163
+ for (const node of ast.program.body) {
164
+ if (node.type !== 'ImportDeclaration') continue;
165
+ const typeOnly = node.importKind === 'type';
166
+ /** @type {ImportStatement} */
167
+ const statement = {
168
+ source: String(node.source.value),
169
+ names: [],
170
+ namespace: false,
171
+ typeOnly,
172
+ };
173
+ for (const s of node.specifiers ?? []) {
174
+ if (s.type === 'ImportDefaultSpecifier') {
175
+ statement.names.push({imported: 'default', typeOnly});
176
+ } else if (s.type === 'ImportNamespaceSpecifier') {
177
+ statement.namespace = true;
178
+ } else if (s.type === 'ImportSpecifier') {
179
+ const imported =
180
+ s.imported.type === 'Identifier'
181
+ ? s.imported.name
182
+ : String(s.imported.value);
183
+ statement.names.push({
184
+ imported,
185
+ typeOnly: typeOnly || s.importKind === 'type',
186
+ });
187
+ }
188
+ }
189
+ out.push(statement);
190
+ }
191
+ return out;
192
+ }
193
+ for (const m of text.matchAll(IMPORT_RE)) {
194
+ const typeOnly = Boolean(m[1]);
195
+ const clause = m[2].trim();
196
+ /** @type {ImportStatement} */
197
+ const statement = {
198
+ source: m[3],
199
+ names: [],
200
+ namespace: /\*\s*as\s/.test(clause),
201
+ typeOnly,
202
+ };
203
+ const braces = /\{([^}]*)\}/.exec(clause);
204
+ const head = clause
205
+ .replace(/\{[^}]*\}/, '')
206
+ .replace(/\*\s*as\s+[\w$]+/, '');
207
+ const defaultName = /^([\w$]+)/.exec(head.trim());
208
+ if (defaultName) statement.names.push({imported: 'default', typeOnly});
209
+ if (braces) {
210
+ for (const part of braces[1].split(',')) {
211
+ const item = part.trim();
212
+ if (!item) continue;
213
+ const t = /^type\s+/.test(item);
214
+ const name = item
215
+ .replace(/^type\s+/, '')
216
+ .split(/\s+as\s+/)[0]
217
+ .trim();
218
+ if (/^[\w$]+$/.test(name))
219
+ statement.names.push({imported: name, typeOnly: typeOnly || t});
220
+ }
221
+ }
222
+ out.push(statement);
223
+ }
224
+ return out;
225
+ }
226
+
227
+ // ── package.json exports ────────────────────────────────────────────
228
+
229
+ /** @typedef {import('./exports-subpaths.mjs').SubpathDeclaration} SubpathDeclaration */
230
+
231
+ /**
232
+ * The files on disk a declared subpath can load, in condition order of the
233
+ * package.json (any condition); empty when none exists yet (build output).
234
+ * @param {string} packageDir
235
+ * @param {SubpathDeclaration} declaration
236
+ * @returns {string[]}
237
+ */
238
+ function existingTargets(packageDir, declaration) {
239
+ if (declaration.state !== 'declared') return [];
240
+ const root = path.resolve(packageDir);
241
+ return allTargets(declaration.value)
242
+ .filter(target => target.startsWith('./'))
243
+ .map(target =>
244
+ path.resolve(root, target.replaceAll('*', declaration.wildcard ?? '')),
245
+ )
246
+ .filter(
247
+ file =>
248
+ (file === root || file.startsWith(root + path.sep)) && isFile(file),
249
+ );
250
+ }
251
+
252
+ /**
253
+ * How a package declares one subpath. A package with no `exports` keeps its
254
+ * legacy deep paths: a subpath that names a file on disk (with or without a
255
+ * module extension, or a folder index) counts as declared.
256
+ * @param {unknown} exportsField
257
+ * @param {string} packageDir
258
+ * @param {string} subpath
259
+ * @returns {SubpathDeclaration}
260
+ */
261
+ function declarationIn(exportsField, packageDir, subpath) {
262
+ const declaration = subpathDeclaration(exportsField, subpath);
263
+ if (declaration.state !== 'no_exports') return declaration;
264
+ if (subpath === '.') return {state: 'declared', value: null, wildcard: ''};
265
+ const root = path.resolve(packageDir);
266
+ const base = path.resolve(root, subpath.slice(2));
267
+ if (!base.startsWith(root + path.sep)) return declaration;
268
+ const file = [
269
+ base,
270
+ ...MODULE_EXTENSIONS.map(ext => `${base}${ext}`),
271
+ ...MODULE_EXTENSIONS.map(ext => path.join(base, `index${ext}`)),
272
+ ].find(isFile);
273
+ return file
274
+ ? {state: 'declared', value: `./${rel(root, file)}`, wildcard: ''}
275
+ : declaration;
276
+ }
277
+
278
+ /**
279
+ * A declared subpath of the package whose target is `file`, if any: an exact
280
+ * key, or a wildcard key whose target pattern matches the file.
281
+ * @param {unknown} exportsField
282
+ * @param {string} packageDir
283
+ * @param {string} file
284
+ * @returns {string | null}
285
+ */
286
+ function subpathFor(exportsField, packageDir, file) {
287
+ if (!isRecord(exportsField)) return null;
288
+ const wanted = `./${rel(packageDir, file)}`;
289
+ for (const [key, value] of Object.entries(exportsField)) {
290
+ if (!key.startsWith('.') || !importable(value)) continue;
291
+ for (const target of allTargets(value)) {
292
+ if (!key.includes('*')) {
293
+ if (target === wanted) return key;
294
+ continue;
295
+ }
296
+ const star = target.indexOf('*');
297
+ if (star === -1) continue;
298
+ const prefix = target.slice(0, star);
299
+ const suffix = target.slice(star + 1);
300
+ if (
301
+ wanted.startsWith(prefix) &&
302
+ wanted.endsWith(suffix) &&
303
+ wanted.length >= prefix.length + suffix.length
304
+ ) {
305
+ return key.replace(
306
+ '*',
307
+ wanted.slice(prefix.length, wanted.length - suffix.length),
308
+ );
309
+ }
310
+ }
311
+ }
312
+ return null;
313
+ }
314
+
315
+ /**
316
+ * The condition names an export value uses, at any depth, in order.
317
+ * @param {unknown} value
318
+ * @param {string[]} [out]
319
+ * @returns {string[]}
320
+ */
321
+ function conditionNames(value, out = []) {
322
+ if (Array.isArray(value)) for (const v of value) conditionNames(v, out);
323
+ else if (isRecord(value)) {
324
+ for (const [key, v] of Object.entries(value)) {
325
+ if (!out.includes(key)) out.push(key);
326
+ conditionNames(v, out);
327
+ }
328
+ }
329
+ return out;
330
+ }
331
+
332
+ /**
333
+ * Why a declared state is not importable, in words.
334
+ * @param {SubpathDeclaration} declaration
335
+ * @param {string} subpath
336
+ */
337
+ function whyUndeclared(declaration, subpath) {
338
+ const {state} = declaration;
339
+ switch (state) {
340
+ case 'no_exports':
341
+ return `package.json has no "exports" and no file exists at "${subpath}"`;
342
+ case 'root_only':
343
+ return `package.json "exports" names only the package root, which blocks every subpath`;
344
+ case 'blocked':
345
+ return `package.json "exports" maps "${subpath}" to null`;
346
+ case 'not_importable': {
347
+ const names = conditionNames(declaration.value);
348
+ const listed = names.map(name => `"${name}"`).join(', ');
349
+ return names.length === 0
350
+ ? `package.json "exports" gives "${subpath}" no target an ES module import can load`
351
+ : `package.json "exports" gives "${subpath}" only the ${listed} condition${names.length === 1 ? '' : 's'}, none of which an ES module import loads`;
352
+ }
353
+ default:
354
+ return `package.json "exports" does not declare "${subpath}"`;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * The paste-ready package.json change that declares `subpath` → `target`.
360
+ * @param {unknown} exportsField
361
+ * @param {SubpathDeclaration['state']} state
362
+ * @param {string} subpath
363
+ * @param {string} target `./`-prefixed package-relative file
364
+ */
365
+ function exportsFix(exportsField, state, subpath, target) {
366
+ const entry = `${JSON.stringify(subpath)}: ${JSON.stringify(target)}`;
367
+ if (state === 'no_exports') {
368
+ return `point it at a file the package ships, or add "exports": {${entry}} to package.json (an "exports" map makes every path it does not list private, so list "." and the package's other public paths too).`;
369
+ }
370
+ if (state === 'root_only') {
371
+ return `make "exports" a subpath map: {".": ${JSON.stringify(exportsField)}, ${entry}}.`;
372
+ }
373
+ if (state === 'not_importable') {
374
+ return `add an ES module condition to "${subpath}" in package.json "exports", e.g. "default": ${JSON.stringify(target)} (or the built file).`;
375
+ }
376
+ if (state === 'blocked') {
377
+ return `replace the null for "${subpath}" in package.json "exports" with ${JSON.stringify(target)}, or stop advertising it.`;
378
+ }
379
+ return `add ${entry} to package.json "exports" (or the built file it compiles to).`;
380
+ }
381
+
382
+ // ── Module export names ─────────────────────────────────────────────
383
+
384
+ /**
385
+ * Export lists read during one public-import check, by file. Scoped to that
386
+ * check, so a later check in the same process rereads every module.
387
+ * @typedef {Map<string, {names: Set<string>, complete: boolean, types: Set<string>, typed: boolean}>} ExportNameCache
388
+ */
389
+
390
+ const DECLARATION_RE = /\.d\.[cm]?ts$/u;
391
+ const DECLARATION_EXTENSIONS = ['.d.ts', '.d.mts', '.d.cts'];
392
+
393
+ /**
394
+ * The declaration file beside a built module (`x.js` → `x.d.ts`), if any.
395
+ * @param {string} file
396
+ * @returns {string | null}
397
+ */
398
+ function declarationTwin(file) {
399
+ const m = /\.([cm]?)jsx?$/u.exec(file);
400
+ if (!m) return null;
401
+ const stem = file.slice(0, m.index);
402
+ return (
403
+ [`.d.${m[1]}ts`, '.d.ts'].map(ext => `${stem}${ext}`).find(isFile) ?? null
404
+ );
405
+ }
406
+
407
+ /** @param {string} specifier @param {string} fromFile */
408
+ function resolveRelative(specifier, fromFile) {
409
+ const base = path.resolve(path.dirname(fromFile), specifier);
410
+ const stripped = base.replace(/\.(?:m?js|jsx|cjs)$/u, '');
411
+ // TypeScript source resolves `./x.js` to `./x.ts` (or .tsx, .d.ts) first.
412
+ if (TYPED_RE.test(fromFile) && !DECLARATION_RE.test(fromFile)) {
413
+ const source = [
414
+ ...['.ts', '.tsx', '.mts', '.cts'].map(ext => `${stripped}${ext}`),
415
+ ...DECLARATION_EXTENSIONS.map(ext => `${stripped}${ext}`),
416
+ ].find(isFile);
417
+ if (source && stripped !== base) return source;
418
+ }
419
+ // A declaration file's `export * from './x.js'` names `./x.d.ts`.
420
+ if (DECLARATION_RE.test(fromFile)) {
421
+ const twin = [
422
+ ...DECLARATION_EXTENSIONS.map(ext => `${stripped}${ext}`),
423
+ ...DECLARATION_EXTENSIONS.map(ext => path.join(base, `index${ext}`)),
424
+ ].find(isFile);
425
+ if (twin) return twin;
426
+ }
427
+ const candidates = [
428
+ base,
429
+ ...MODULE_EXTENSIONS.map(ext => `${base}${ext}`),
430
+ ...MODULE_EXTENSIONS.map(ext => `${stripped}${ext}`),
431
+ ...MODULE_EXTENSIONS.map(ext => path.join(base, `index${ext}`)),
432
+ ];
433
+ return candidates.find(isFile) ?? null;
434
+ }
435
+
436
+ /**
437
+ * Every name (runtime or type) a module exports, and whether the list is
438
+ * complete: an `export *` from another package, `export =`, or a module that
439
+ * does not parse leaves it incomplete, and then no name is judged missing.
440
+ * @param {string} file
441
+ * @param {ExportNameCache} [cache] export lists already read in this check
442
+ * @returns {{names: Set<string>, complete: boolean, types: Set<string>, typed: boolean}}
443
+ * `types`: the names exported only as types (an interface, a type alias,
444
+ * `export type`); `typed`: the list includes every type the module
445
+ * exports (TypeScript source, or a JavaScript module whose declaration
446
+ * file was read in full), so a name absent from it is absent as a type too
447
+ */
448
+ export function moduleExportNames(file, cache = new Map()) {
449
+ const {names, complete, types, typed} = readExportNames(file, cache, []);
450
+ return {names, complete, types, typed};
451
+ }
452
+
453
+ /** TypeScript source or a declaration file: its exports include its types. */
454
+ const TYPED_RE = /\.(?:[cm]?tsx?)$/u;
455
+
456
+ /**
457
+ * One module's export list, following local `export *` declarations. `stack`
458
+ * holds the modules whose lists are being read: an `export *` that reaches one
459
+ * of them closes a cycle, and a list read through a cycle the module does not
460
+ * close is not cached, since it lacks what the module on the stack exports.
461
+ * @param {string} file
462
+ * @param {ExportNameCache} cache
463
+ * @param {string[]} stack
464
+ * @returns {{names: Set<string>, complete: boolean, types: Set<string>, typed: boolean, cycleTo: Set<string>}}
465
+ */
466
+ function readExportNames(file, cache, stack) {
467
+ if (stack.includes(file)) {
468
+ return {
469
+ names: new Set(),
470
+ complete: true,
471
+ types: new Set(),
472
+ typed: true,
473
+ cycleTo: new Set([file]),
474
+ };
475
+ }
476
+ const cached = cache.get(file);
477
+ if (cached) return {...cached, cycleTo: new Set()};
478
+ const own = readOwnExportNames(file, cache, stack);
479
+ const {names, types, cycleTo} = own;
480
+ let {complete, typed} = own;
481
+ // A JavaScript module's declaration file lists its types, at every level
482
+ // an `export *` reaches.
483
+ const twin = declarationTwin(file);
484
+ if (twin && !stack.includes(twin)) {
485
+ const declared = readExportNames(twin, cache, [...stack, file]);
486
+ for (const name of declared.names) names.add(name);
487
+ for (const name of declared.types) types.add(name);
488
+ for (const each of declared.cycleTo) cycleTo.add(each);
489
+ if (declared.complete && declared.typed) {
490
+ complete = true;
491
+ typed = true;
492
+ }
493
+ }
494
+ cycleTo.delete(file);
495
+ const value = {names, complete, types, typed};
496
+ if (cycleTo.size === 0) cache.set(file, value);
497
+ return {...value, cycleTo};
498
+ }
499
+
500
+ /**
501
+ * The exports one module itself declares, following local `export *` (each
502
+ * through {@link readExportNames}); its declaration file is merged by the
503
+ * caller.
504
+ * @param {string} file
505
+ * @param {ExportNameCache} cache
506
+ * @param {string[]} stack
507
+ * @returns {{names: Set<string>, complete: boolean, types: Set<string>, typed: boolean, cycleTo: Set<string>}}
508
+ */
509
+ function readOwnExportNames(file, cache, stack) {
510
+ /** @type {Set<string>} */
511
+ const names = new Set();
512
+ /** @type {Set<string>} */
513
+ const types = new Set();
514
+ /** @type {Set<string>} */
515
+ const cycleTo = new Set();
516
+ let typed = TYPED_RE.test(file);
517
+ /** @param {boolean} complete */
518
+ const done = complete => ({names, complete, types, typed, cycleTo});
519
+ // A CommonJS module's exports cannot be read from declarations: leave it
520
+ // unjudged rather than read it as exporting nothing. That is any .cjs/.cts
521
+ // file, any file (built `dist/*.js` included) with no ES module import or
522
+ // export, and any file that also assigns `module.exports` or `exports.*`.
523
+ if (/\.c[jt]s$/u.test(file)) return done(false);
524
+ let complete = true;
525
+ let ast;
526
+ let text;
527
+ try {
528
+ text = fs.readFileSync(file, 'utf-8');
529
+ ast = parseModuleText(text);
530
+ if (ast.errors?.length) complete = false;
531
+ } catch {
532
+ return done(false);
533
+ }
534
+ const esm = ast.program.body.some(
535
+ (/** @type {any} */ node) =>
536
+ node.type === 'ImportDeclaration' ||
537
+ node.type === 'ExportNamedDeclaration' ||
538
+ node.type === 'ExportDefaultDeclaration' ||
539
+ node.type === 'ExportAllDeclaration' ||
540
+ node.type === 'TSExportAssignment',
541
+ );
542
+ // A declaration file with no import or export is a global script: no
543
+ // module import can read it (TypeScript: "is not a module"). Not judged.
544
+ if (!esm) return done(false);
545
+ if (/\b(?:module\.exports|exports\.[\w$]+)\s*=/u.test(text)) {
546
+ return done(false);
547
+ }
548
+ // A declaration file that is a module exports every top-level declaration,
549
+ // marked or not, unless it has an export list (`export {}`, `export {…}
550
+ // from`), `export *`, `export =`, or a default export of an expression
551
+ // (TypeScript's rule for ambient modules). A default-exported declaration
552
+ // (`export default function`, `class`, `interface`) does not end it.
553
+ if (DECLARATION_RE.test(file)) {
554
+ const implicit = !ast.program.body.some(
555
+ (/** @type {any} */ node) =>
556
+ (node.type === 'ExportNamedDeclaration' && !node.declaration) ||
557
+ (node.type === 'ExportDefaultDeclaration' &&
558
+ ![
559
+ 'FunctionDeclaration',
560
+ 'TSDeclareFunction',
561
+ 'ClassDeclaration',
562
+ 'TSInterfaceDeclaration',
563
+ ].includes(node.declaration?.type)) ||
564
+ node.type === 'TSExportAssignment' ||
565
+ node.type === 'ExportAllDeclaration',
566
+ );
567
+ if (implicit) {
568
+ for (const node of ast.program.body) {
569
+ const d = /** @type {any} */ (
570
+ node.type === 'ExportNamedDeclaration' ? node.declaration : node
571
+ );
572
+ if (!d) continue;
573
+ if (d.type === 'VariableDeclaration') {
574
+ for (const v of d.declarations ?? []) {
575
+ if (v.id?.type === 'Identifier') names.add(v.id.name);
576
+ else complete = false;
577
+ }
578
+ } else if (
579
+ d.type === 'TSInterfaceDeclaration' ||
580
+ d.type === 'TSTypeAliasDeclaration'
581
+ ) {
582
+ names.add(d.id.name);
583
+ types.add(d.id.name);
584
+ } else if (
585
+ [
586
+ 'FunctionDeclaration',
587
+ 'TSDeclareFunction',
588
+ 'ClassDeclaration',
589
+ 'TSEnumDeclaration',
590
+ 'TSModuleDeclaration',
591
+ ].includes(d.type) &&
592
+ d.id?.type === 'Identifier'
593
+ ) {
594
+ names.add(d.id.name);
595
+ }
596
+ }
597
+ }
598
+ }
599
+ for (const node of ast.program.body) {
600
+ if (node.type === 'ExportDefaultDeclaration') names.add('default');
601
+ else if (node.type === 'TSExportAssignment') complete = false;
602
+ else if (
603
+ node.type === 'TSImportEqualsDeclaration' &&
604
+ /** @type {any} */ (node).isExport
605
+ ) {
606
+ // `export import Alias = Inner.Value` exports Alias. Whether a
607
+ // type-only one names a value cannot be read, so the list is no longer
608
+ // complete.
609
+ names.add(/** @type {any} */ (node).id.name);
610
+ if (/** @type {any} */ (node).importKind === 'type') {
611
+ types.add(/** @type {any} */ (node).id.name);
612
+ complete = false;
613
+ }
614
+ } else if (node.type === 'ExportNamedDeclaration') {
615
+ const d = /** @type {any} */ (node.declaration);
616
+ const typeOnly = /** @type {any} */ (node).exportKind === 'type';
617
+ if (d) {
618
+ if (d.id?.name) {
619
+ names.add(d.id.name);
620
+ if (
621
+ typeOnly ||
622
+ d.type === 'TSInterfaceDeclaration' ||
623
+ d.type === 'TSTypeAliasDeclaration'
624
+ ) {
625
+ types.add(d.id.name);
626
+ }
627
+ }
628
+ for (const v of d.declarations ?? []) {
629
+ if (v.id?.type === 'Identifier') names.add(v.id.name);
630
+ else complete = false;
631
+ }
632
+ }
633
+ for (const sp of /** @type {any[]} */ (node.specifiers ?? [])) {
634
+ const exported = sp.exported?.name ?? sp.exported?.value;
635
+ if (!exported) continue;
636
+ names.add(exported);
637
+ if (typeOnly || sp.exportKind === 'type') types.add(exported);
638
+ }
639
+ } else if (node.type === 'ExportAllDeclaration') {
640
+ const n = /** @type {any} */ (node);
641
+ if (n.exported) {
642
+ names.add(n.exported.name ?? n.exported.value);
643
+ continue;
644
+ }
645
+ const source = String(n.source.value);
646
+ const target = source.startsWith('.')
647
+ ? resolveRelative(source, file)
648
+ : null;
649
+ if (!target) {
650
+ complete = false;
651
+ continue;
652
+ }
653
+ const inner = readExportNames(target, cache, [...stack, file]);
654
+ for (const name of inner.names) if (name !== 'default') names.add(name);
655
+ for (const name of inner.types) types.add(name);
656
+ if (!inner.complete) complete = false;
657
+ if (!inner.typed) typed = false;
658
+ for (const each of inner.cycleTo) cycleTo.add(each);
659
+ }
660
+ }
661
+ return done(complete);
662
+ }
663
+
664
+ /**
665
+ * Every file a declared subpath names, and whether each is on disk.
666
+ * @param {string} packageDir
667
+ * @param {SubpathDeclaration} declaration
668
+ * @returns {{files: string[], allPresent: boolean}}
669
+ */
670
+ function declaredTargets(packageDir, declaration) {
671
+ if (declaration.state !== 'declared') return {files: [], allPresent: false};
672
+ const root = path.resolve(packageDir);
673
+ const all = allTargets(declaration.value)
674
+ .filter(target => target.startsWith('./'))
675
+ .map(target =>
676
+ path.resolve(root, target.replaceAll('*', declaration.wildcard ?? '')),
677
+ )
678
+ .filter(file => file === root || file.startsWith(root + path.sep));
679
+ const files = all.filter(isFile);
680
+ return {files, allPresent: files.length === all.length};
681
+ }
682
+
683
+ /**
684
+ * The names an import statement asks for that the module provably lacks.
685
+ * A name is missing only when every file the subpath names is on disk and
686
+ * read in full, and at least one of those lists includes its types: any
687
+ * construct the reader does not model leaves the import unchecked.
688
+ * @param {ImportStatement} statement
689
+ * @param {string[]} files candidate targets on disk
690
+ * @param {ExportNameCache} cache
691
+ * @param {boolean} [allPresent] every target the subpath names is on disk
692
+ * @param {boolean} [values] every name is known to be a value (a component),
693
+ * so a list without the module's types can still judge it
694
+ * @returns {string[] | null} null when it cannot be judged
695
+ */
696
+ function missingNames(
697
+ statement,
698
+ files,
699
+ cache,
700
+ allPresent = true,
701
+ values = false,
702
+ ) {
703
+ // A type-only name may live only in a declaration file: not judged.
704
+ const wanted = statement.names
705
+ .filter(binding => !binding.typeOnly)
706
+ .map(binding => binding.imported);
707
+ if (wanted.length === 0) return [];
708
+ if (files.length === 0) return null;
709
+ // Any condition's file (source or built) that has every name settles it.
710
+ let incomplete = false;
711
+ let typed = false;
712
+ /** @type {Set<string>} */
713
+ const seen = new Set();
714
+ for (const file of files) {
715
+ const list = moduleExportNames(file, cache);
716
+ if (wanted.every(name => list.names.has(name))) return [];
717
+ if (!list.complete) incomplete = true;
718
+ if (list.typed) typed = true;
719
+ for (const name of list.names) seen.add(name);
720
+ }
721
+ // A target not built yet, a list cut short, or one without the module's
722
+ // types (a name written without `type` may be a type) is not judged.
723
+ if (incomplete || (!typed && !values) || !allPresent) return null;
724
+ return wanted.filter(name => !seen.has(name));
725
+ }
726
+
727
+ const AUGMENTATION_HINT_RE = /\bdeclare\s+module\b/u;
728
+ const TYPESCRIPT_FILE_RE = /\.(?:d\.[cm]?ts|[cm]?ts|tsx)$/u;
729
+ const AUGMENTATION_WALK_LIMIT = 5000;
730
+
731
+ /**
732
+ * Whether a TypeScript file has a real `declare module '<spec>'` block for a
733
+ * module of this package: a relative spec, or the package's own name or a
734
+ * subpath of it. Comments, `declare global`, wildcard specs (`'*.css'`) and
735
+ * other packages' names do not count. A `.tsx` file is parsed with JSX, any
736
+ * other TypeScript file without it (so `<T>value` assertions parse); a file
737
+ * with `declare module` in its text that still fails to parse counts as
738
+ * augmenting.
739
+ * @param {Context} ctx
740
+ * @param {string} text
741
+ * @param {string} file
742
+ */
743
+ function augmentsOwnModule(ctx, text, file) {
744
+ if (!AUGMENTATION_HINT_RE.test(text)) return false;
745
+ let body;
746
+ try {
747
+ body = parseModuleText(text, {jsx: file.endsWith('.tsx')}).program.body;
748
+ } catch {
749
+ return true;
750
+ }
751
+ return body.some(node => {
752
+ const d = node.type === 'ExportNamedDeclaration' ? node.declaration : node;
753
+ if (d?.type !== 'TSModuleDeclaration' || d.id?.type !== 'StringLiteral')
754
+ return false;
755
+ const spec = String(d.id.value);
756
+ if (spec.includes('*')) return false;
757
+ if (spec.startsWith('./') || spec.startsWith('../')) return true;
758
+ return (
759
+ ctx.name != null && (spec === ctx.name || spec.startsWith(`${ctx.name}/`))
760
+ );
761
+ });
762
+ }
763
+
764
+ /**
765
+ * The first TypeScript file in the package (outside `node_modules` and dot
766
+ * folders) with a `declare module '…'` block for a module of the package,
767
+ * read once per check. Such a block can add names to that module, so while
768
+ * one exists no name is judged missing: what TypeScript merges is not
769
+ * modelled. A package too large to walk counts as augmented.
770
+ * @param {Context} ctx
771
+ * @returns {string | null} the reason names were not checked, or null
772
+ */
773
+ function augmentationReason(ctx) {
774
+ if (ctx.augmentation !== undefined) return ctx.augmentation;
775
+ const root = path.resolve(ctx.packageDir);
776
+ /** @type {string[]} */
777
+ const queue = [root];
778
+ let seen = 0;
779
+ /** @type {string | null} */
780
+ let reason = null;
781
+ walk: while (queue.length > 0) {
782
+ const current = /** @type {string} */ (queue.shift());
783
+ let entries;
784
+ try {
785
+ entries = fs.readdirSync(current, {withFileTypes: true});
786
+ } catch {
787
+ continue;
788
+ }
789
+ entries.sort((x, y) => x.name.localeCompare(y.name));
790
+ for (const entry of entries) {
791
+ if (++seen > AUGMENTATION_WALK_LIMIT) {
792
+ reason = `the package has more than ${AUGMENTATION_WALK_LIMIT} files to search for a \`declare module\` block, which can add names to its modules, so the names it imports were not checked`;
793
+ break walk;
794
+ }
795
+ const full = path.join(current, entry.name);
796
+ if (entry.isDirectory()) {
797
+ if (entry.name !== 'node_modules' && !entry.name.startsWith('.'))
798
+ queue.push(full);
799
+ continue;
800
+ }
801
+ if (!entry.isFile() || !TYPESCRIPT_FILE_RE.test(entry.name)) continue;
802
+ let text;
803
+ try {
804
+ text = fs.readFileSync(full, 'utf-8');
805
+ } catch {
806
+ continue;
807
+ }
808
+ if (augmentsOwnModule(ctx, text, entry.name)) {
809
+ reason = `"${rel(root, full)}" has a \`declare module\` block for a module of the package, which can add names to it, so the names it imports were not checked`;
810
+ break walk;
811
+ }
812
+ }
813
+ }
814
+ ctx.augmentation = reason;
815
+ return reason;
816
+ }
817
+
818
+ /**
819
+ * Why an import's names could not be judged.
820
+ * @param {string[]} files
821
+ * @param {boolean} [allPresent]
822
+ */
823
+ function uncheckedReason(files, allPresent = true) {
824
+ if (files.length === 0 || !allPresent) {
825
+ return 'a file its package.json "exports" entry names does not exist yet (build output), so the names it imports were not checked';
826
+ }
827
+ return "its module's export list cannot be read in full (CommonJS, an `export *` from another package, a parse error, or JavaScript with no declaration file for its types), so the names it imports were not checked";
828
+ }
829
+
830
+ /**
831
+ * Whether `name` reads as a component: PascalCase, not ALL CAPS.
832
+ * @param {string} name
833
+ */
834
+ function isComponentName(name) {
835
+ return /^[A-Z]/u.test(name) && /[a-z]/u.test(name);
836
+ }
837
+
838
+ /**
839
+ * The export a template's default export should be: an exported function or
840
+ * class first, else an exported `const`, each only when its name is
841
+ * PascalCase (not ALL CAPS) and not only a type; null when there is none to
842
+ * suggest (a hook such as `useAccount`, a helper such as `formatBalance`, a
843
+ * constant such as `ACCOUNT_LIMIT`), and then the fix stays generic.
844
+ * @param {string} text the template source
845
+ * @param {{names: Set<string>, types: Set<string>}} exported
846
+ * @returns {string | null}
847
+ */
848
+ function defaultExportCandidate(text, exported) {
849
+ let body;
850
+ try {
851
+ body = parseModuleText(text).program.body;
852
+ } catch {
853
+ return null;
854
+ }
855
+ /** @type {Map<string, 'callable' | 'const'>} top-level bindings by kind */
856
+ const local = new Map();
857
+ /** @param {any} d */
858
+ const record = d => {
859
+ if (
860
+ (d?.type === 'FunctionDeclaration' || d?.type === 'ClassDeclaration') &&
861
+ d.id?.name
862
+ ) {
863
+ local.set(d.id.name, 'callable');
864
+ } else if (d?.type === 'VariableDeclaration' && d.kind === 'const') {
865
+ for (const v of d.declarations ?? []) {
866
+ if (v.id?.type === 'Identifier') local.set(v.id.name, 'const');
867
+ }
868
+ }
869
+ };
870
+ for (const node of body) {
871
+ record(node.type === 'ExportNamedDeclaration' ? node.declaration : node);
872
+ }
873
+ /** @type {{name: string, kind: 'callable' | 'const'}[]} */
874
+ const candidates = [];
875
+ for (const node of body) {
876
+ if (node.type !== 'ExportNamedDeclaration') continue;
877
+ const n = /** @type {any} */ (node);
878
+ if (n.exportKind === 'type') continue;
879
+ const d = n.declaration;
880
+ /** @type {string[]} */
881
+ const names = d
882
+ ? d.id?.name
883
+ ? [d.id.name]
884
+ : (d.declarations ?? [])
885
+ .map((/** @type {any} */ v) => v.id?.name)
886
+ .filter(Boolean)
887
+ : n.source
888
+ ? []
889
+ : (n.specifiers ?? [])
890
+ .filter((/** @type {any} */ sp) => sp.exportKind !== 'type')
891
+ .filter(
892
+ (/** @type {any} */ sp) =>
893
+ (sp.exported?.name ?? sp.exported?.value) === sp.local?.name,
894
+ )
895
+ .map((/** @type {any} */ sp) => sp.local.name);
896
+ for (const name of names) {
897
+ const kind = local.get(name);
898
+ if (kind) candidates.push({name, kind});
899
+ }
900
+ }
901
+ const fits = (/** @type {{name: string}} */ c) =>
902
+ isComponentName(c.name) && !exported.types.has(c.name);
903
+ return (
904
+ candidates.find(c => c.kind === 'callable' && fits(c))?.name ??
905
+ candidates.find(c => c.kind === 'const' && fits(c))?.name ??
906
+ null
907
+ );
908
+ }
909
+
910
+ /**
911
+ * Whether `name` is a bare npm package name (an optional `@scope/` and a
912
+ * name), the only form an import can name the package by.
913
+ * @param {string} name
914
+ */
915
+ function isPackageName(name) {
916
+ try {
917
+ normalizeProviderId(name);
918
+ return true;
919
+ } catch {
920
+ return false;
921
+ }
922
+ }
923
+
924
+ // ── Listed findings ─────────────────────────────────────────────────
925
+
926
+ /**
927
+ * A public-import finding as a note on validate's result: listed with its
928
+ * fix inside the message, never counted or failed on. It becomes a failing
929
+ * warning in the next minor release (a new failure code joins only in a
930
+ * minor).
931
+ * @param {string} code
932
+ * @param {string} message
933
+ * @returns {PublicImportNote}
934
+ */
935
+ function listed(code, message) {
936
+ return {code, message};
937
+ }
938
+
939
+ // ── Checks ──────────────────────────────────────────────────────────
940
+
941
+ /**
942
+ * @typedef {object} Context
943
+ * @property {string} packageDir
944
+ * @property {string | null} name the package name; null when package.json has
945
+ * none, and then no import of the package's own name is judged
946
+ * @property {unknown} exportsField
947
+ * @property {string | null} coreDir
948
+ * @property {any} corePkg
949
+ * @property {PublicImportUnchecked[]} unchecked what could not be judged
950
+ * @property {ExportNameCache} exportNames export lists read in this check
951
+ * @property {string | null} [augmentation] why no name is judged missing
952
+ * (a module augmentation in the package); null when none, unset until read
953
+ * @property {string[]} files the package.json `files` entries, normalized
954
+ * @property {string[]} roots the integration's contribution roots, absolute
955
+ */
956
+
957
+ const BUILT_EXTENSION_RE = /\.[cm]?jsx?$/u;
958
+
959
+ /**
960
+ * Whether an undeclared deep path, in a package with no `exports`, points
961
+ * into build output that is not built yet: its top folder is missing, or is
962
+ * a `files` folder that holds no contribution root. Such an import cannot be
963
+ * judged from source.
964
+ * @param {Context} ctx
965
+ * @param {string} subpath
966
+ * @param {SubpathDeclaration} declaration
967
+ */
968
+ function unbuiltOutput(ctx, subpath, declaration) {
969
+ if (declaration.state !== 'no_exports' || subpath === '.') return false;
970
+ const top = subpath.slice(2).split('/')[0];
971
+ if (!top) return false;
972
+ const topDir = path.resolve(ctx.packageDir, top);
973
+ if (!fs.existsSync(topDir)) return true;
974
+ return (
975
+ ctx.files.includes(top) &&
976
+ !ctx.roots.some(
977
+ root => root === topDir || root.startsWith(topDir + path.sep),
978
+ )
979
+ );
980
+ }
981
+
982
+ /**
983
+ * The reason an import into unbuilt output is listed as not checked.
984
+ * @param {string} subpath
985
+ */
986
+ function unbuiltReason(subpath) {
987
+ return `package.json has no "exports", and "${subpath}" is build output that is not built yet, so it was not checked`;
988
+ }
989
+
990
+ /**
991
+ * The target a fix maps `subpath` to: the component's source for a source
992
+ * path, never a TypeScript source for a built `.js` path.
993
+ * @param {string} subpath
994
+ * @param {string | null} sourceTarget
995
+ */
996
+ function fixTarget(subpath, sourceTarget) {
997
+ if (
998
+ sourceTarget &&
999
+ !(
1000
+ BUILT_EXTENSION_RE.test(subpath) && !BUILT_EXTENSION_RE.test(sourceTarget)
1001
+ )
1002
+ ) {
1003
+ return sourceTarget;
1004
+ }
1005
+ return BUILT_EXTENSION_RE.test(subpath)
1006
+ ? `<the built file "${subpath}" names>`
1007
+ : `./${subpath.slice(2)}.tsx`;
1008
+ }
1009
+
1010
+ /** @typedef {import('../../api/integration/validate-integration.type.mjs').PublicImportUnchecked} PublicImportUnchecked */
1011
+ /** @typedef {import('../../api/integration/validate-integration.type.mjs').PublicImportsReport} PublicImportsReport */
1012
+ /** @typedef {{code: string, message: string}} PublicImportNote */
1013
+
1014
+ /**
1015
+ * Whether nothing a statement imports exists at run time: a statement-level
1016
+ * `import type`, or named bindings that are each an inline `type`.
1017
+ * @param {ImportStatement} statement
1018
+ */
1019
+ function typeOnlyStatement(statement) {
1020
+ if (statement.typeOnly) return true;
1021
+ return (
1022
+ !statement.namespace &&
1023
+ statement.names.length > 0 &&
1024
+ statement.names.every(binding => binding.typeOnly)
1025
+ );
1026
+ }
1027
+
1028
+ /**
1029
+ * The files the `types` conditions of an export value name that an ES module
1030
+ * import can reach (never one under `require`), with a wildcard filled in.
1031
+ * @param {unknown} value
1032
+ * @param {string} wildcard
1033
+ * @param {boolean} [underTypes]
1034
+ * @param {string[]} [out]
1035
+ * @returns {string[]}
1036
+ */
1037
+ function typesTargets(value, wildcard, underTypes = false, out = []) {
1038
+ if (typeof value === 'string') {
1039
+ if (underTypes) out.push(value.replace('*', wildcard));
1040
+ } else if (Array.isArray(value)) {
1041
+ for (const v of value) typesTargets(v, wildcard, underTypes, out);
1042
+ } else if (isRecord(value)) {
1043
+ for (const [condition, v] of Object.entries(value)) {
1044
+ if (condition === 'require') continue;
1045
+ typesTargets(v, wildcard, underTypes || condition === 'types', out);
1046
+ }
1047
+ }
1048
+ return out;
1049
+ }
1050
+
1051
+ /**
1052
+ * The names a declaration file exports as types: exported interfaces and
1053
+ * type aliases, `export type {…}`, and `export {…}` of a local interface or
1054
+ * type alias. Anything else (a value, an `export *`) is not counted, so an
1055
+ * import of it is listed as not checked. Null when the file cannot be read.
1056
+ * @param {string} file
1057
+ * @returns {Set<string> | null}
1058
+ */
1059
+ function declaredTypeNames(file) {
1060
+ let ast;
1061
+ try {
1062
+ ast = parse(fs.readFileSync(file, 'utf-8'), {
1063
+ sourceType: 'module',
1064
+ errorRecovery: true,
1065
+ plugins: /** @type {any} */ ([
1066
+ ['typescript', {dts: /\.d\.[cm]?ts$/u.test(file)}],
1067
+ ]),
1068
+ });
1069
+ } catch {
1070
+ return null;
1071
+ }
1072
+ /** @type {Set<string>} */
1073
+ const local = new Set();
1074
+ /** @type {Set<string>} */
1075
+ const names = new Set();
1076
+ /** @param {any} node */
1077
+ const typeName = node =>
1078
+ node &&
1079
+ (node.type === 'TSInterfaceDeclaration' ||
1080
+ node.type === 'TSTypeAliasDeclaration')
1081
+ ? node.id.name
1082
+ : null;
1083
+ for (const node of ast.program.body) {
1084
+ const own = typeName(node);
1085
+ if (own) local.add(own);
1086
+ if (node.type === 'ExportNamedDeclaration') {
1087
+ const declared = typeName(node.declaration);
1088
+ if (declared) local.add(declared);
1089
+ }
1090
+ }
1091
+ /** @param {any} n */
1092
+ const exportedName = n =>
1093
+ n.type === 'Identifier' ? n.name : String(n.value);
1094
+ for (const node of ast.program.body) {
1095
+ if (node.type !== 'ExportNamedDeclaration') continue;
1096
+ const declared = typeName(node.declaration);
1097
+ if (declared) names.add(declared);
1098
+ for (const s of node.specifiers ?? []) {
1099
+ if (s.type !== 'ExportSpecifier') continue;
1100
+ const exported = exportedName(s.exported);
1101
+ if (node.exportKind === 'type' || s.exportKind === 'type')
1102
+ names.add(exported);
1103
+ else if (!node.source && local.has(exportedName(s.local)))
1104
+ names.add(exported);
1105
+ }
1106
+ }
1107
+ return names;
1108
+ }
1109
+
1110
+ /**
1111
+ * An import of a subpath whose `exports` entry has only `types` targets is
1112
+ * never judged as a run-time import: accepted when it is `import type`, its
1113
+ * every binding is an inline `type`, or every name it imports is one the
1114
+ * `types` file declares as a type; otherwise the reason it was not checked.
1115
+ * @param {Context} ctx
1116
+ * @param {ImportStatement} statement
1117
+ * @param {string} subpath
1118
+ * @param {SubpathDeclaration} declaration
1119
+ * @returns {string | null}
1120
+ */
1121
+ function typesOnlyImport(ctx, statement, subpath, declaration) {
1122
+ if (typeOnlyStatement(statement)) return null;
1123
+ const not = `"${subpath}" reaches an ES module import only through a "types" target in package.json "exports", which nothing loads at run time`;
1124
+ if (statement.namespace || statement.names.length === 0)
1125
+ return `${not}, and a namespace or side-effect import of it cannot be judged from source, so it was not checked`;
1126
+ const files = typesTargets(declaration.value, declaration.wildcard ?? '');
1127
+ /** @type {Set<string>} */
1128
+ const types = new Set();
1129
+ let read = false;
1130
+ for (const target of files) {
1131
+ const found = declaredTypeNames(path.resolve(ctx.packageDir, target));
1132
+ if (!found) continue;
1133
+ read = true;
1134
+ for (const n of found) types.add(n);
1135
+ }
1136
+ if (!read)
1137
+ return `${not}, and its types file ${files.map(f => `"${f}"`).join(', ')} could not be read (it may be build output), so it was not checked`;
1138
+ const unknown = statement.names
1139
+ .filter(binding => !binding.typeOnly && !types.has(binding.imported))
1140
+ .map(binding => binding.imported);
1141
+ if (unknown.length === 0) return null;
1142
+ return `${not}, and its types file does not declare ${unknown.join(', ')} as a type, so it was not checked`;
1143
+ }
1144
+
1145
+ /**
1146
+ * How an import of the package's own name is judged: the declaration when
1147
+ * `exports` does not declare its subpath, the names its module provably
1148
+ * lacks, or the reason it was not checked.
1149
+ * @param {Context} ctx
1150
+ * @param {ImportStatement} statement
1151
+ * @param {string} subpath
1152
+ * @returns {{undeclared: SubpathDeclaration | null, missing: string[] | null, reason: string | null}}
1153
+ */
1154
+ function judgeOwnImport(ctx, statement, subpath) {
1155
+ const declaration = declarationIn(ctx.exportsField, ctx.packageDir, subpath);
1156
+ // A subpath an ES module import reaches only through `types` (any other
1157
+ // condition beside it, such as `require`, loads nothing for it) is judged as
1158
+ // a type import, never as a run-time one.
1159
+ if (
1160
+ declaration.state === 'not_importable' &&
1161
+ typesTargets(declaration.value, declaration.wildcard ?? '').length > 0
1162
+ ) {
1163
+ const reason = typesOnlyImport(ctx, statement, subpath, declaration);
1164
+ return {undeclared: null, missing: [], reason};
1165
+ }
1166
+ if (declaration.state !== 'declared')
1167
+ return {undeclared: declaration, missing: null, reason: null};
1168
+ if (subpath === '.' && declaration.value == null) {
1169
+ return {
1170
+ undeclared: null,
1171
+ missing: null,
1172
+ reason:
1173
+ 'package.json has no "exports", and the root module "main" names is not read, so the names it imports were not checked',
1174
+ };
1175
+ }
1176
+ const {files, allPresent} = declaredTargets(ctx.packageDir, declaration);
1177
+ const missing = missingNames(statement, files, ctx.exportNames, allPresent);
1178
+ const augmented = missing?.length ? augmentationReason(ctx) : null;
1179
+ if (augmented) return {undeclared: null, missing: null, reason: augmented};
1180
+ return {
1181
+ undeclared: null,
1182
+ missing,
1183
+ reason: missing == null ? uncheckedReason(files, allPresent) : null,
1184
+ };
1185
+ }
1186
+
1187
+ /**
1188
+ * A declared subpath of the package whose module, read in full, exports
1189
+ * `name`: the one the component's source is exported under first.
1190
+ * @param {Context} ctx
1191
+ * @param {string} name
1192
+ * @param {string | null} preferred
1193
+ * @returns {string | null}
1194
+ */
1195
+ function subpathExporting(ctx, name, preferred) {
1196
+ if (!isRecord(ctx.exportsField)) return null;
1197
+ /** @param {string} subpath */
1198
+ const exportsName = subpath =>
1199
+ existingTargets(
1200
+ ctx.packageDir,
1201
+ declarationIn(ctx.exportsField, ctx.packageDir, subpath),
1202
+ ).some(file => moduleExportNames(file, ctx.exportNames).names.has(name));
1203
+ // The source's own subpath, which may come from a wildcard key, first.
1204
+ if (preferred && exportsName(preferred)) return preferred;
1205
+ for (const key of Object.keys(ctx.exportsField)) {
1206
+ if (!key.startsWith('.') || key.includes('*') || key === preferred)
1207
+ continue;
1208
+ if (exportsName(key)) return key;
1209
+ }
1210
+ return null;
1211
+ }
1212
+
1213
+ /**
1214
+ * A component doc's import specifier, and every example's imports.
1215
+ * @param {Context} ctx
1216
+ * @param {any} integration
1217
+ * @returns {Promise<PublicImportNote[]>}
1218
+ */
1219
+ async function componentIssues(ctx, integration) {
1220
+ /** @type {PublicImportNote[]} */
1221
+ const issues = [];
1222
+ // Every component check compares imports with the package's own name.
1223
+ if (ctx.name == null) return issues;
1224
+ let records;
1225
+ try {
1226
+ records = discoverIntegrationComponents(integration);
1227
+ } catch {
1228
+ return issues; // an unreadable root is reported by the root validators
1229
+ }
1230
+ /** @type {Map<string, typeof records>} */
1231
+ const byDoc = new Map();
1232
+ for (const record of records) {
1233
+ const list = byDoc.get(record.docPath) ?? [];
1234
+ list.push(record);
1235
+ byDoc.set(record.docPath, list);
1236
+ }
1237
+ for (const [docPath, list] of byDoc) {
1238
+ let doc;
1239
+ try {
1240
+ doc = await loadComponentDoc(docPath);
1241
+ } catch {
1242
+ continue; // reported by the doc validators
1243
+ }
1244
+ if (!isRecord(doc)) continue;
1245
+ const shownDoc = rel(ctx.packageDir, docPath);
1246
+ const record = list[0];
1247
+ const source = record.sourcePath;
1248
+ const sourceTarget = source ? `./${rel(ctx.packageDir, source)}` : null;
1249
+ // The import the doc writes, or the one the CLI derives for it (its
1250
+ // folder's subpath when exported, else the package root).
1251
+ const authored = typeof doc.import === 'string' ? doc.import : null;
1252
+ const specifier =
1253
+ authored ??
1254
+ resolveIntegrationImportPath(
1255
+ {
1256
+ exportsMap: /** @type {any} */ (ctx.exportsField) ?? null,
1257
+ packageDir: ctx.packageDir,
1258
+ docPath,
1259
+ packageName: ctx.name,
1260
+ },
1261
+ record.name,
1262
+ );
1263
+ const split = splitSpecifier(specifier);
1264
+ // The entry that exports this component alone, and the doc import that
1265
+ // uses it: never one component's file as the package root or folder.
1266
+ const entryKey = sourceTarget
1267
+ ? sourceTarget.replace(/\.[cm]?[jt]sx?$/u, '')
1268
+ : null;
1269
+ const perComponent = entryKey
1270
+ ? `add "${entryKey}": "${sourceTarget}" to package.json "exports" and set import: '${ctx.name}${entryKey.slice(1)}' in the doc`
1271
+ : null;
1272
+ const gives = authored
1273
+ ? `gives import "${specifier}"`
1274
+ : `has no import, so apps are told to import from "${specifier}"`;
1275
+ const declared = source
1276
+ ? subpathFor(ctx.exportsField, ctx.packageDir, source)
1277
+ : null;
1278
+ const suggested =
1279
+ declared ??
1280
+ (sourceTarget ? sourceTarget.replace(/\.[cm]?[jt]sx?$/u, '') : null);
1281
+ if (split && split.pkg !== ctx.name) {
1282
+ issues.push(
1283
+ listed(
1284
+ 'component_import_other_package',
1285
+ `Component doc "${shownDoc}" ${gives}, which names package "${split.pkg}", not this package "${ctx.name}", so apps are told to import the component from somewhere it is not. Fix: set import: '${ctx.name}${suggested ? suggested.slice(1) : ''}' in the doc.`,
1286
+ ),
1287
+ );
1288
+ } else if (split) {
1289
+ const declaration = declarationIn(
1290
+ ctx.exportsField,
1291
+ ctx.packageDir,
1292
+ split.subpath,
1293
+ );
1294
+ if (unbuiltOutput(ctx, split.subpath, declaration)) {
1295
+ ctx.unchecked.push({
1296
+ file: shownDoc,
1297
+ specifier,
1298
+ reason: unbuiltReason(split.subpath),
1299
+ });
1300
+ } else if (
1301
+ declaration.state === 'declared' &&
1302
+ split.subpath === '.' &&
1303
+ declaration.value == null
1304
+ ) {
1305
+ // A package with no exports resolves its root through main: not read.
1306
+ ctx.unchecked.push({
1307
+ file: shownDoc,
1308
+ specifier,
1309
+ reason:
1310
+ 'package.json has no "exports", and the root module "main" names is not read, so the component names were not checked',
1311
+ });
1312
+ } else if (declaration.state === 'declared') {
1313
+ // A doc whose `name` differs from the name it is listed under is
1314
+ // judged by the doc-name check (`doc_name_mismatch`), never here.
1315
+ const judged = list.filter(
1316
+ r => typeof doc.name !== 'string' || doc.name === r.name,
1317
+ );
1318
+ const {files, allPresent} = declaredTargets(
1319
+ ctx.packageDir,
1320
+ declaration,
1321
+ );
1322
+ const statement = {
1323
+ source: specifier,
1324
+ names: judged.map(r => ({imported: r.name, typeOnly: false})),
1325
+ namespace: false,
1326
+ };
1327
+ // A component is a run-time export, which no augmentation adds, so
1328
+ // a missing one is reported whatever the package declares.
1329
+ const missing =
1330
+ judged.length === 0
1331
+ ? []
1332
+ : missingNames(statement, files, ctx.exportNames, allPresent, true);
1333
+ if (missing == null) {
1334
+ ctx.unchecked.push({
1335
+ file: shownDoc,
1336
+ specifier,
1337
+ reason: uncheckedReason(files, allPresent),
1338
+ });
1339
+ }
1340
+ for (const name of missing ?? []) {
1341
+ const there = subpathExporting(ctx, name, declared);
1342
+ // The per-component entry helps only when the component's source
1343
+ // exports the name itself and the doc does not already use it.
1344
+ const ownEntry =
1345
+ perComponent != null &&
1346
+ source != null &&
1347
+ entryKey !== split.subpath &&
1348
+ moduleExportNames(source, ctx.exportNames).names.has(name);
1349
+ const fix = there
1350
+ ? `set import: '${ctx.name}${there === '.' ? '' : there.slice(1)}' in the doc (that subpath exports it), or export "${name}" from the module "${specifier}" loads.`
1351
+ : `export "${name}" from the module "${specifier}" loads${ownEntry ? `, or ${perComponent}` : ''}.`;
1352
+ issues.push(
1353
+ listed(
1354
+ 'component_import_missing_name',
1355
+ `Component doc "${shownDoc}" ${gives}, but that module does not export "${name}", so apps that follow the doc get nothing. Fix: ${fix}`,
1356
+ ),
1357
+ );
1358
+ }
1359
+ } else {
1360
+ const already =
1361
+ declared && declared !== split.subpath
1362
+ ? `set import: '${ctx.name}${declared.slice(1)}' (already exported), or `
1363
+ : '';
1364
+ issues.push(
1365
+ listed(
1366
+ 'component_import_not_exported',
1367
+ `Component doc "${shownDoc}" ${gives}, but ${whyUndeclared(declaration, split.subpath)}, so apps cannot import the component from it. Fix: ${already}${split.subpath === '.' && perComponent ? `${perComponent}.` : exportsFix(ctx.exportsField, declaration.state, split.subpath, fixTarget(split.subpath, sourceTarget))}`,
1368
+ ),
1369
+ );
1370
+ }
1371
+ }
1372
+
1373
+ /**
1374
+ * Whether the module at a declared subpath exports every value name the
1375
+ * statement imports (and it imports at least one).
1376
+ * @param {ImportStatement} statement
1377
+ * @param {string} candidate
1378
+ */
1379
+ const providesAll = (statement, candidate) => {
1380
+ if (!statement.names.some(binding => !binding.typeOnly)) return false;
1381
+ const declaration = declarationIn(
1382
+ ctx.exportsField,
1383
+ ctx.packageDir,
1384
+ candidate,
1385
+ );
1386
+ if (declaration.state !== 'declared' || declaration.value == null)
1387
+ return false;
1388
+ const {files, allPresent} = declaredTargets(ctx.packageDir, declaration);
1389
+ const missing = missingNames(
1390
+ statement,
1391
+ files,
1392
+ ctx.exportNames,
1393
+ allPresent,
1394
+ );
1395
+ return Array.isArray(missing) && missing.length === 0;
1396
+ };
1397
+
1398
+ /**
1399
+ * Where an example with an undeclared import should import from: the
1400
+ * doc's own import, when it differs from the example's and is declared;
1401
+ * else the subpath the component's source is already exported under;
1402
+ * else the exact `exports` entry to add for the example's subpath.
1403
+ * Another subpath is offered only when the statement imports at least one
1404
+ * value name and its module provably exports every one of them.
1405
+ * @param {ImportStatement} statement
1406
+ * @param {string} subpath
1407
+ * @param {SubpathDeclaration} undeclared
1408
+ */
1409
+ const exampleFix = (statement, subpath, undeclared) => {
1410
+ /** @param {string} candidate */
1411
+ const provides = candidate => providesAll(statement, candidate);
1412
+ const values = statement.names
1413
+ .filter(binding => !binding.typeOnly)
1414
+ .map(binding => binding.imported);
1415
+ // A statement that imports no value name (a side-effect, namespace or
1416
+ // all-type import) is never sent to another subpath: no names prove it.
1417
+ const own = authored ? splitSpecifier(authored) : null;
1418
+ if (
1419
+ values.length > 0 &&
1420
+ authored &&
1421
+ authored !== statement.source &&
1422
+ own?.pkg === ctx.name &&
1423
+ provides(own.subpath)
1424
+ ) {
1425
+ return `import from '${authored}' in the example, as the doc's import does.`;
1426
+ }
1427
+ if (
1428
+ values.length > 0 &&
1429
+ declared &&
1430
+ declared !== subpath &&
1431
+ provides(declared)
1432
+ ) {
1433
+ return `import from '${ctx.name}${declared.slice(1)}' in the example (already exported).`;
1434
+ }
1435
+ // The entry for the example's own subpath: the file there, or the
1436
+ // component's source when it exports every name, else a placeholder.
1437
+ const atSubpath = [
1438
+ '',
1439
+ ...MODULE_EXTENSIONS,
1440
+ ...MODULE_EXTENSIONS.map(ext => `/index${ext}`),
1441
+ ]
1442
+ .map(ext => path.resolve(ctx.packageDir, subpath.slice(2) + ext))
1443
+ .find(
1444
+ file =>
1445
+ file.startsWith(path.resolve(ctx.packageDir) + path.sep) &&
1446
+ isFile(file),
1447
+ );
1448
+ const sourceHasAll =
1449
+ values.length > 0 &&
1450
+ source != null &&
1451
+ values.every(name =>
1452
+ moduleExportNames(source, ctx.exportNames).names.has(name),
1453
+ );
1454
+ const target = atSubpath
1455
+ ? `./${rel(ctx.packageDir, atSubpath)}`
1456
+ : sourceHasAll && sourceTarget
1457
+ ? fixTarget(subpath, sourceTarget)
1458
+ : values.length > 0
1459
+ ? `<the file that exports ${values.join(', ')}>`
1460
+ : '<the file it loads>';
1461
+ return exportsFix(ctx.exportsField, undeclared.state, subpath, target);
1462
+ };
1463
+
1464
+ /**
1465
+ * The fix for an example that imports names its module lacks: import
1466
+ * this doc's components that module does export, else the doc's own
1467
+ * import when it differs, else export the names from it.
1468
+ * @param {ImportStatement} statement
1469
+ * @param {string} subpath
1470
+ * @param {string[]} missing
1471
+ */
1472
+ const missingNameFix = (statement, subpath, missing) => {
1473
+ const declaration = declarationIn(
1474
+ ctx.exportsField,
1475
+ ctx.packageDir,
1476
+ subpath,
1477
+ );
1478
+ /** @type {Set<string>} */
1479
+ const there = new Set();
1480
+ for (const file of existingTargets(ctx.packageDir, declaration)) {
1481
+ for (const name of moduleExportNames(file, ctx.exportNames).names)
1482
+ there.add(name);
1483
+ }
1484
+ const docNames = list.map(r => r.name).filter(name => there.has(name));
1485
+ const exportIt = `export ${missing.length === 1 ? 'the name' : 'the names'} from it`;
1486
+ if (docNames.length > 0) {
1487
+ return `import a name that module exports (${docNames.map(n => `"${n}"`).join(', ')} for this doc), or ${exportIt}.`;
1488
+ }
1489
+ const own = authored ? splitSpecifier(authored) : null;
1490
+ if (
1491
+ authored &&
1492
+ authored !== statement.source &&
1493
+ own?.pkg === ctx.name &&
1494
+ providesAll(statement, own.subpath)
1495
+ ) {
1496
+ return `import from '${authored}', as the doc's import does, or ${exportIt}.`;
1497
+ }
1498
+ return `${exportIt[0].toUpperCase()}${exportIt.slice(1)}.`;
1499
+ };
1500
+
1501
+ const examples = Array.isArray(doc.examples) ? doc.examples : [];
1502
+ for (const [index, example] of examples.entries()) {
1503
+ if (!isRecord(example) || typeof example.code !== 'string') continue;
1504
+ const label =
1505
+ typeof example.label === 'string'
1506
+ ? `"${example.label}"`
1507
+ : `#${index + 1}`;
1508
+ for (const statement of readImports(example.code)) {
1509
+ const parts = splitSpecifier(statement.source);
1510
+ if (!parts || parts.pkg !== ctx.name) continue;
1511
+ const {undeclared, missing, reason} = judgeOwnImport(
1512
+ ctx,
1513
+ statement,
1514
+ parts.subpath,
1515
+ );
1516
+ if (reason) {
1517
+ ctx.unchecked.push({
1518
+ file: shownDoc,
1519
+ specifier: statement.source,
1520
+ reason: `example ${label}: ${reason}`,
1521
+ });
1522
+ }
1523
+ if (undeclared && unbuiltOutput(ctx, parts.subpath, undeclared)) {
1524
+ ctx.unchecked.push({
1525
+ file: shownDoc,
1526
+ specifier: statement.source,
1527
+ reason: `example ${label}: ${unbuiltReason(parts.subpath)}`,
1528
+ });
1529
+ } else if (undeclared) {
1530
+ issues.push(
1531
+ listed(
1532
+ 'example_import_not_exported',
1533
+ `Component doc "${shownDoc}" example ${label} imports "${statement.source}", but ${whyUndeclared(undeclared, parts.subpath)}, so the example fails when an app copies it. Fix: ${exampleFix(statement, parts.subpath, undeclared)}`,
1534
+ ),
1535
+ );
1536
+ } else if (missing && missing.length > 0) {
1537
+ issues.push(
1538
+ listed(
1539
+ 'example_import_missing_name',
1540
+ `Component doc "${shownDoc}" example ${label} imports ${missing.map(n => `"${n}"`).join(', ')} from "${statement.source}", which does not export ${missing.length === 1 ? 'it' : 'them'}, so the example fails when an app copies it. Fix: ${missingNameFix(statement, parts.subpath, missing)}`,
1541
+ ),
1542
+ );
1543
+ }
1544
+ }
1545
+ }
1546
+ }
1547
+ return issues;
1548
+ }
1549
+
1550
+ /**
1551
+ * Template public imports and what each template source imports.
1552
+ * @param {Context} ctx
1553
+ * @param {any} integration
1554
+ * @returns {Promise<PublicImportNote[]>}
1555
+ */
1556
+ async function templateIssues(ctx, integration) {
1557
+ /** @type {PublicImportNote[]} */
1558
+ const issues = [];
1559
+ const root = integration.templates;
1560
+ if (!root) return issues;
1561
+ let templates;
1562
+ try {
1563
+ ({templates} = await discoverIntegrationTemplatesForOne(integration));
1564
+ } catch {
1565
+ return issues;
1566
+ }
1567
+ for (const template of templates) {
1568
+ const id = template.dirName;
1569
+ const file = template.filePath;
1570
+ const shown = rel(ctx.packageDir, file);
1571
+ const sourceTarget = `./${shown}`;
1572
+ const subpath = `./${shown.replace(/\.tsx?$/u, '')}`;
1573
+ const declaration = declarationIn(
1574
+ ctx.exportsField,
1575
+ ctx.packageDir,
1576
+ subpath,
1577
+ );
1578
+ if (ctx.name != null && declaration.state !== 'declared') {
1579
+ issues.push(
1580
+ listed(
1581
+ 'template_import_not_exported',
1582
+ `Template "${id}" has public import "${ctx.name}${subpath.slice(1)}", but ${whyUndeclared(declaration, subpath)}, so it cannot be imported once published. Fix: ${exportsFix(ctx.exportsField, declaration.state, subpath, sourceTarget)}`,
1583
+ ),
1584
+ );
1585
+ }
1586
+
1587
+ let text;
1588
+ try {
1589
+ text = fs.readFileSync(file, 'utf-8');
1590
+ } catch {
1591
+ text = '';
1592
+ }
1593
+ // `astryx template` copies the source as written, but the package's
1594
+ // public import, the gallery preview and a page that renders the
1595
+ // template import its default export.
1596
+ const exported = text ? moduleExportNames(file, ctx.exportNames) : null;
1597
+ if (exported?.complete && !exported.names.has('default')) {
1598
+ const named = defaultExportCandidate(text, exported);
1599
+ const fix = named
1600
+ ? `add \`export default ${named};\` to ${shown}, or make ${named} the default export`
1601
+ : `export the template's component as the default export of ${shown} (\`export default function …\`)`;
1602
+ // With no package name there is no public import to name.
1603
+ const what =
1604
+ ctx.name == null
1605
+ ? 'anything that renders the template gets nothing'
1606
+ : `its public import "${ctx.name}${subpath.slice(1)}" and anything that renders the template get nothing`;
1607
+ issues.push(
1608
+ listed(
1609
+ 'template_no_default_export',
1610
+ `Template "${id}" (${shown}) has no default export, so ${what}. Fix: ${fix}.`,
1611
+ ),
1612
+ );
1613
+ }
1614
+ for (const statement of readImports(text)) {
1615
+ const spec = statement.source;
1616
+ if (spec.startsWith('.')) {
1617
+ const target = path.resolve(path.dirname(file), spec);
1618
+ if (target !== root && !target.startsWith(root + path.sep)) {
1619
+ issues.push(
1620
+ listed(
1621
+ 'template_import_outside_root',
1622
+ `Template "${id}" (${shown}) imports "${spec}", a path outside the templates root; \`astryx template\` copies only the template into an app, where that path does not exist. Fix: import it through the package's public name (e.g. '${ctx.name ?? '<package name>'}/…' declared in package.json "exports"), or move the code into the template.`,
1623
+ ),
1624
+ );
1625
+ }
1626
+ continue;
1627
+ }
1628
+ const parts = splitSpecifier(spec);
1629
+ if (!parts) continue;
1630
+ if (ctx.name != null && parts.pkg === ctx.name) {
1631
+ const {undeclared, missing, reason} = judgeOwnImport(
1632
+ ctx,
1633
+ statement,
1634
+ parts.subpath,
1635
+ );
1636
+ if (reason) {
1637
+ ctx.unchecked.push({file: shown, specifier: spec, reason});
1638
+ }
1639
+ if (undeclared && unbuiltOutput(ctx, parts.subpath, undeclared)) {
1640
+ ctx.unchecked.push({
1641
+ file: shown,
1642
+ specifier: spec,
1643
+ reason: unbuiltReason(parts.subpath),
1644
+ });
1645
+ } else if (undeclared) {
1646
+ issues.push(
1647
+ listed(
1648
+ 'template_private_import',
1649
+ `Template "${id}" (${shown}) imports "${spec}", but ${whyUndeclared(undeclared, parts.subpath)}, so the template breaks in an app. Fix: import from a subpath package.json "exports" declares, or add one for it.`,
1650
+ ),
1651
+ );
1652
+ } else if (missing && missing.length > 0) {
1653
+ issues.push(
1654
+ listed(
1655
+ 'template_import_missing_name',
1656
+ `Template "${id}" (${shown}) imports ${missing.map(n => `"${n}"`).join(', ')} from "${spec}", which does not export ${missing.length === 1 ? 'it' : 'them'}, so the template breaks in an app. Fix: import a name that module exports, or export the name from it.`,
1657
+ ),
1658
+ );
1659
+ }
1660
+ } else if (parts.pkg === CORE && !ctx.corePkg) {
1661
+ ctx.unchecked.push({
1662
+ file: shown,
1663
+ specifier: spec,
1664
+ reason:
1665
+ '@astryxdesign/core is not installed, so Core\'s package.json "exports" could not be read',
1666
+ });
1667
+ } else if (parts.pkg === CORE && ctx.corePkg && parts.subpath !== '.') {
1668
+ const core = declarationIn(
1669
+ ctx.corePkg.exports,
1670
+ /** @type {string} */ (ctx.coreDir),
1671
+ parts.subpath,
1672
+ );
1673
+ if (core.state !== 'declared') {
1674
+ const top = parts.subpath
1675
+ .split('/')
1676
+ .filter(s => s && s !== '.' && s !== 'src')[0];
1677
+ const hint =
1678
+ top &&
1679
+ subpathDeclaration(ctx.corePkg.exports, `./${top}`).state ===
1680
+ 'declared'
1681
+ ? `'${CORE}/${top}'`
1682
+ : `a subpath Core's package.json "exports" declares`;
1683
+ issues.push(
1684
+ listed(
1685
+ 'template_private_import',
1686
+ `Template "${id}" (${shown}) imports "${spec}", a path Core's package.json "exports" does not declare, so the template breaks in an app. Fix: import from ${hint}.`,
1687
+ ),
1688
+ );
1689
+ }
1690
+ }
1691
+ }
1692
+ }
1693
+ return issues;
1694
+ }
1695
+
1696
+ /**
1697
+ * Every public-import finding for one package, as notes, and the report of
1698
+ * what could not be judged. It returns no issues: the notes are listed
1699
+ * only, and fail `--strict` from the next minor release.
1700
+ * @param {string} packageDir
1701
+ * @param {any} integration the loaded integration record
1702
+ * @returns {Promise<{issues: Issue[], notes: PublicImportNote[], report: PublicImportsReport}>}
1703
+ * `issues` is always empty: kept so validate's call site stays one shape
1704
+ */
1705
+ export async function publicImportIssues(packageDir, integration) {
1706
+ /** @type {PublicImportsReport} */
1707
+ const report = {status: 'checked', findings: [], unchecked: []};
1708
+ const pkg = readJson(path.join(packageDir, 'package.json'));
1709
+ // What cannot run is listed as not checked, never a skip `--strict` would
1710
+ // fail on.
1711
+ if (!isRecord(pkg)) {
1712
+ report.unchecked.push({
1713
+ file: 'package.json',
1714
+ specifier: '',
1715
+ reason: 'package.json could not be read, so no import was checked',
1716
+ });
1717
+ return {issues: [], notes: [], report};
1718
+ }
1719
+ const raw = typeof pkg.name === 'string' && pkg.name !== '' ? pkg.name : null;
1720
+ // A name that is not a bare package name (`acme/kit`) cannot be told apart
1721
+ // from an import of another package: checked as if there were none.
1722
+ const name = raw != null && isPackageName(raw) ? raw : null;
1723
+ if (name == null) {
1724
+ report.unchecked.push({
1725
+ file: 'package.json',
1726
+ specifier: '',
1727
+ reason: `${raw == null ? 'package.json has no "name"' : `package.json "name" ${JSON.stringify(raw)} is not a bare package name`}, so imports of the package's own name were not checked; only template imports outside the templates root and of Core were`,
1728
+ });
1729
+ }
1730
+ // The Core installed beside the package, which its templates' imports
1731
+ // resolve against; never the CLI's own copy, which may be another release.
1732
+ // A workspace's own `packages/core` (another package) is not it: then Core
1733
+ // counts as not installed and its imports are listed as not checked.
1734
+ const foundCore = name === CORE ? packageDir : findProjectCoreDir(packageDir);
1735
+ const foundCorePkg = foundCore
1736
+ ? readJson(path.join(foundCore, 'package.json'))
1737
+ : null;
1738
+ const isCore = isRecord(foundCorePkg) && foundCorePkg.name === CORE;
1739
+ /** @type {Context} */
1740
+ const ctx = {
1741
+ packageDir,
1742
+ name,
1743
+ exportsField: pkg.exports,
1744
+ coreDir: isCore ? foundCore : null,
1745
+ corePkg: isCore ? foundCorePkg : null,
1746
+ unchecked: report.unchecked,
1747
+ exportNames: new Map(),
1748
+ files: Array.isArray(pkg.files)
1749
+ ? pkg.files
1750
+ .filter(entry => typeof entry === 'string')
1751
+ .map(entry => entry.replace(/^\.\//u, '').replace(/\/+$/u, ''))
1752
+ : [],
1753
+ roots: [
1754
+ integration.components,
1755
+ integration.templates,
1756
+ integration.docs,
1757
+ integration.themes,
1758
+ integration.codemods,
1759
+ ]
1760
+ .filter(root => typeof root === 'string')
1761
+ .map(root => path.resolve(root)),
1762
+ };
1763
+ const notes = [
1764
+ ...(await componentIssues(ctx, integration)),
1765
+ ...(await templateIssues(ctx, integration)),
1766
+ ];
1767
+ // Listed only: none is an issue, so none counts or fails a check.
1768
+ return {issues: [], notes, report};
1769
+ }