@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
@@ -51,6 +51,8 @@ import {
51
51
  validateInstalledIntegration,
52
52
  validateLocalIntegration,
53
53
  } from './validate-integration.mjs';
54
+ import {rootProblem} from '../../foundation/integrations/validate-contributions.mjs';
55
+ import {normalizeProviderId} from '../../foundation/identity/provider-identity.mjs';
54
56
 
55
57
  /** @param {string} value */
56
58
  export function shellArg(value) {
@@ -58,15 +60,61 @@ export function shellArg(value) {
58
60
  return `'${value.replace(/'/g, `'\\''`)}'`;
59
61
  }
60
62
 
63
+ /**
64
+ * The provider ID a package without a name is checked under: a valid bare
65
+ * package name no published package can hold (npm names are lowercase).
66
+ */
67
+ const UNNAMED_PROVIDER = 'astryx-unnamed-local-package';
68
+
69
+ /**
70
+ * A message as the package sees it: the stand-in provider ID gone, its docs
71
+ * named by their path in the package.
72
+ * @param {string} message
73
+ */
74
+ function unnamedShown(message) {
75
+ return message
76
+ .split(`${UNNAMED_PROVIDER}/`)
77
+ .join('')
78
+ .split(UNNAMED_PROVIDER)
79
+ .join('this package');
80
+ }
81
+
61
82
  /**
62
83
  * Resolve one local or installed integration for an authoring check.
63
84
  * @param {string | undefined} pkg
64
85
  * @param {string} cwd
65
86
  */
66
87
  async function resolveIntegration(pkg, cwd) {
67
- return pkg
68
- ? validateInstalledIntegration(pkg, cwd)
69
- : validateLocalIntegration(cwd);
88
+ const resolved = pkg
89
+ ? await validateInstalledIntegration(pkg, cwd)
90
+ : await validateLocalIntegration(cwd);
91
+ const integration = resolved.found ? resolved.integration : undefined;
92
+ if (!integration) return resolved;
93
+ // A root that is not a readable folder is reported in the issues; the
94
+ // checks read nothing from it rather than fail on it.
95
+ /** @type {typeof integration & {__unnamed?: boolean}} */
96
+ const usable = {...integration};
97
+ // The provider ID readers key the package on: the manifest's providerId,
98
+ // else its name. A package with neither (validate warned about it) is
99
+ // still checked; the docs check names its docs under a stand-in ID.
100
+ try {
101
+ usable.providerId = normalizeProviderId(
102
+ integration.__declaredProviderId ?? integration.name,
103
+ );
104
+ } catch {
105
+ usable.__unnamed = true;
106
+ }
107
+ for (const kind of /** @type {const} */ ([
108
+ 'components',
109
+ 'templates',
110
+ 'codemods',
111
+ 'docs',
112
+ 'themes',
113
+ ])) {
114
+ const root = usable[kind];
115
+ if (root != null && rootProblem(root)) usable[kind] = undefined;
116
+ }
117
+ return {...resolved, integration: usable};
70
118
  }
71
119
 
72
120
  /**
@@ -387,8 +435,20 @@ export async function integrationDocConflicts(pkg, options = {}) {
387
435
  };
388
436
  }
389
437
 
438
+ // The docs tree names a package's docs by its provider ID, which a
439
+ // package.json without a name does not give: its docs are checked under a
440
+ // stand-in ID, which no message names.
441
+ const unnamed = /** @type {{__unnamed?: boolean}} */ (resolved.integration)
442
+ .__unnamed;
443
+ const docsIntegration = unnamed
444
+ ? {
445
+ ...resolved.integration,
446
+ name: UNNAMED_PROVIDER,
447
+ providerId: UNNAMED_PROVIDER,
448
+ }
449
+ : resolved.integration;
390
450
  const [discovered, builtinTopics] = await Promise.all([
391
- discoverIntegrationDocs(resolved.integration),
451
+ discoverIntegrationDocs(docsIntegration),
392
452
  Promise.resolve(discoverBuiltinTopics()),
393
453
  ]);
394
454
  const {records, errors} = discovered;
@@ -458,27 +518,40 @@ export async function integrationDocConflicts(pkg, options = {}) {
458
518
  // its docs (spec:AST-046, spec:AST-047).
459
519
  if (errors.length === 0) {
460
520
  for (const {severity, message} of await packageDocsProblems(
461
- /** @type {{name: string}} */ (resolved.integration),
521
+ /** @type {{name: string}} */ (docsIntegration),
462
522
  discovered,
463
523
  )) {
464
- issues.push({code: 'invalid_doc_graph', severity, message});
524
+ issues.push({
525
+ code: 'invalid_doc_graph',
526
+ severity,
527
+ message: unnamedShown(message),
528
+ });
465
529
  }
466
530
 
467
531
  // A reference block includes content rather than linking to it, so one
468
532
  // that cannot include what it names loses that content for every reader:
469
533
  // an error, where a link that names no doc still prints as written.
470
534
  for (const message of await packageReferenceProblems(
471
- /** @type {{name: string}} */ (resolved.integration),
535
+ /** @type {{name: string}} */ (docsIntegration),
472
536
  discovered,
473
537
  )) {
474
- issues.push({code: 'invalid_doc_reference', severity: 'error', message});
538
+ issues.push({
539
+ code: 'invalid_doc_reference',
540
+ severity: 'error',
541
+ message: unnamedShown(message),
542
+ });
475
543
  }
476
544
 
477
545
  // One fact has one owner: a passage two topics repeat goes stale in one
478
546
  // of them. Each is listed and is a warning (architecture:cli-surface
479
547
  // DEC-8), so `integration check --strict` fails until it is fixed;
480
548
  // `doctor integration docs` keeps its exit code, as it fails on errors.
481
- copiedPassages.push(...(await integrationCopiedPassages(discovered)));
549
+ copiedPassages.push(
550
+ ...(await integrationCopiedPassages(discovered)).map(passage => ({
551
+ ...passage,
552
+ message: unnamedShown(passage.message),
553
+ })),
554
+ );
482
555
  for (const {message} of copiedPassages) {
483
556
  issues.push({code: 'copied_passage', severity: 'warning', message});
484
557
  }
@@ -488,6 +561,7 @@ export async function integrationDocConflicts(pkg, options = {}) {
488
561
  ...(await integrationFluff(discovered)).map(each => ({
489
562
  ...each,
490
563
  file: fromPackage(each.file),
564
+ message: unnamedShown(each.message),
491
565
  })),
492
566
  );
493
567
  for (const {message} of fluff) {
@@ -14,6 +14,7 @@ vi.mock('../../foundation/discovery/template-conflict-release.mjs', () => ({
14
14
  }));
15
15
  import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
+ import {validateIntegration} from './validate-integration.mjs';
17
18
  import {discoverCoreTemplates} from '../../foundation/discovery/template-adapter.mjs';
18
19
  import {discoverOwnedComponents} from '../../foundation/discovery/component-discovery.mjs';
19
20
  import {discoverBuiltinTopics} from '../../foundation/discovery/docs-discovery.mjs';
@@ -1331,3 +1332,222 @@ describe('links in component docs and topic descriptions', () => {
1331
1332
  ]);
1332
1333
  });
1333
1334
  });
1335
+
1336
+ describe('validateIntegration runner-command notes', () => {
1337
+ /** The runner-command notes, read back into their parts. */
1338
+ async function runnerNotes() {
1339
+ const {data} = await validateIntegration(undefined, {cwd: tmpDir});
1340
+ const notes = (data.notes ?? [])
1341
+ .filter((/** @type {any} */ note) => note.code === 'runner_command')
1342
+ .map((/** @type {any} */ note) => {
1343
+ const m =
1344
+ /^(\S+): "([^"]+)"(?: \(section "([^"]+)"\))? writes `([^`]+)`(?: \(line (\d+)\))?\./.exec(
1345
+ note.message,
1346
+ );
1347
+ return {
1348
+ file: m?.[1],
1349
+ topic: m?.[2],
1350
+ section: m?.[3] ?? null,
1351
+ command: m?.[4],
1352
+ line: m?.[5] ? Number(m[5]) : null,
1353
+ message: note.message,
1354
+ };
1355
+ });
1356
+ return {data, notes};
1357
+ }
1358
+
1359
+ function writeTopic(/** @type {string} */ text) {
1360
+ fs.writeFileSync(
1361
+ path.join(tmpDir, 'package.json'),
1362
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
1363
+ );
1364
+ fs.writeFileSync(
1365
+ path.join(tmpDir, 'astryx.integration.mjs'),
1366
+ `export default {docs: './docs'};\n`,
1367
+ );
1368
+ fs.mkdirSync(path.join(tmpDir, 'docs'), {recursive: true});
1369
+ const doc = {
1370
+ type: 'generic',
1371
+ name: 'acme-setup',
1372
+ title: 'Fixture',
1373
+ description: 'Fixture topic.',
1374
+ sections: [
1375
+ {
1376
+ title: 'Overview',
1377
+ content: [
1378
+ {type: 'prose', text},
1379
+ {type: 'code', lang: 'bash', code: 'npx @astryxdesign/cli init'},
1380
+ ],
1381
+ },
1382
+ ],
1383
+ };
1384
+ fs.writeFileSync(
1385
+ path.join(tmpDir, 'docs', 'acme-setup.doc.mjs'),
1386
+ `export default ${JSON.stringify(doc)};\n`,
1387
+ );
1388
+ }
1389
+
1390
+ it('lists a runner command with its line and fix, and raises no issue', async () => {
1391
+ writeTopic('Check it with `pnpm exec astryx doctor` first.');
1392
+
1393
+ const {data, notes} = await runnerNotes();
1394
+ const [hit, ...rest] = notes;
1395
+
1396
+ expect(rest).toEqual([]);
1397
+ expect(hit).toMatchObject({
1398
+ topic: 'acme-setup',
1399
+ section: 'Overview',
1400
+ file: 'docs/acme-setup.doc.mjs',
1401
+ line: 1,
1402
+ command: 'pnpm exec astryx doctor',
1403
+ });
1404
+ expect(hit.message).toContain('Write `astryx doctor`');
1405
+ // A note only: no issue, so no exit changes on it.
1406
+ expect(
1407
+ data.issues.map((/** @type {any} */ issue) => issue.code),
1408
+ ).not.toContain('runner_command');
1409
+ });
1410
+
1411
+ it('puts each copy of a repeated command on its own line', async () => {
1412
+ fs.writeFileSync(
1413
+ path.join(tmpDir, 'package.json'),
1414
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
1415
+ );
1416
+ fs.writeFileSync(
1417
+ path.join(tmpDir, 'astryx.integration.mjs'),
1418
+ `export default {docs: './docs'};\n`,
1419
+ );
1420
+ fs.mkdirSync(path.join(tmpDir, 'docs'), {recursive: true});
1421
+ const doc = {
1422
+ type: 'generic',
1423
+ name: 'acme-setup',
1424
+ title: 'Fixture',
1425
+ description: 'Fixture topic.',
1426
+ sections: [
1427
+ {
1428
+ title: 'First',
1429
+ content: [{type: 'prose', text: 'Run `npx astryx doctor`.'}],
1430
+ },
1431
+ {
1432
+ title: 'Second',
1433
+ content: [{type: 'prose', text: 'Run `npx astryx doctor` again.'}],
1434
+ },
1435
+ {
1436
+ title: 'Third',
1437
+ content: [{type: 'prose', text: 'Or plain `npx astryx`.'}],
1438
+ },
1439
+ ],
1440
+ };
1441
+ fs.writeFileSync(
1442
+ path.join(tmpDir, 'docs', 'acme-setup.doc.mjs'),
1443
+ `export default ${JSON.stringify(doc, null, 2)};\n`,
1444
+ );
1445
+ const lines = fs
1446
+ .readFileSync(path.join(tmpDir, 'docs', 'acme-setup.doc.mjs'), 'utf8')
1447
+ .split('\n');
1448
+ const lineHolding = (/** @type {string} */ text) =>
1449
+ lines.findIndex(line => line.includes(text)) + 1;
1450
+
1451
+ const {notes} = await runnerNotes();
1452
+
1453
+ expect(
1454
+ notes.map((/** @type {any} */ each) => [
1455
+ each.section,
1456
+ each.line,
1457
+ each.command,
1458
+ ]),
1459
+ ).toEqual([
1460
+ ['First', lineHolding('Run `npx astryx doctor`.'), 'npx astryx doctor'],
1461
+ ['Second', lineHolding('again.'), 'npx astryx doctor'],
1462
+ ['Third', lineHolding('Or plain'), 'npx astryx'],
1463
+ ]);
1464
+ });
1465
+
1466
+ it('puts each copy of an identical one-line string on its own line', async () => {
1467
+ fs.writeFileSync(
1468
+ path.join(tmpDir, 'package.json'),
1469
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
1470
+ );
1471
+ fs.writeFileSync(
1472
+ path.join(tmpDir, 'astryx.integration.mjs'),
1473
+ `export default {docs: './docs'};\n`,
1474
+ );
1475
+ fs.mkdirSync(path.join(tmpDir, 'docs'), {recursive: true});
1476
+ const block = {type: 'code', lang: 'bash', code: 'npx astryx doctor'};
1477
+ const doc = {
1478
+ type: 'generic',
1479
+ name: 'acme-setup',
1480
+ title: 'Fixture',
1481
+ description: 'Fixture topic.',
1482
+ sections: [
1483
+ {title: 'First', content: [block]},
1484
+ {title: 'Second', content: [block]},
1485
+ ],
1486
+ };
1487
+ fs.writeFileSync(
1488
+ path.join(tmpDir, 'docs', 'acme-setup.doc.mjs'),
1489
+ `export default ${JSON.stringify(doc, null, 2)};\n`,
1490
+ );
1491
+ const lines = fs
1492
+ .readFileSync(path.join(tmpDir, 'docs', 'acme-setup.doc.mjs'), 'utf8')
1493
+ .split('\n');
1494
+ const copies = lines.flatMap((line, at) =>
1495
+ line.includes('"npx astryx doctor"') ? [at + 1] : [],
1496
+ );
1497
+
1498
+ const {notes} = await runnerNotes();
1499
+
1500
+ expect(copies).toHaveLength(2);
1501
+ expect(
1502
+ notes.map((/** @type {any} */ each) => [each.section, each.line]),
1503
+ ).toEqual([
1504
+ ['First', copies[0]],
1505
+ ['Second', copies[1]],
1506
+ ]);
1507
+ });
1508
+
1509
+ it("places a section's command on its own line when the section sits in a constant above the doc", async () => {
1510
+ fs.writeFileSync(
1511
+ path.join(tmpDir, 'package.json'),
1512
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
1513
+ );
1514
+ fs.writeFileSync(
1515
+ path.join(tmpDir, 'astryx.integration.mjs'),
1516
+ `export default {docs: './docs'};\n`,
1517
+ );
1518
+ fs.mkdirSync(path.join(tmpDir, 'docs'), {recursive: true});
1519
+ fs.writeFileSync(
1520
+ path.join(tmpDir, 'docs', 'acme-setup.doc.mjs'),
1521
+ [
1522
+ "const install = {title: 'Install', content: [{type: 'prose', text: 'Run `npx astryx doctor`.'}]};",
1523
+ '',
1524
+ 'export default {',
1525
+ " type: 'generic',",
1526
+ " name: 'acme-setup',",
1527
+ " title: 'Fixture',",
1528
+ " description: 'Install first: `npx astryx doctor`.',",
1529
+ ' sections: [install],',
1530
+ '};',
1531
+ '',
1532
+ ].join('\n'),
1533
+ );
1534
+
1535
+ const {notes} = await runnerNotes();
1536
+
1537
+ expect(
1538
+ notes.map((/** @type {any} */ each) => [each.section, each.line]),
1539
+ ).toEqual(
1540
+ expect.arrayContaining([
1541
+ [null, 7],
1542
+ ['Install', 1],
1543
+ ]),
1544
+ );
1545
+ });
1546
+
1547
+ it('lists nothing for bare commands and the scoped install step', async () => {
1548
+ writeTopic('Check it with `astryx doctor` first.');
1549
+
1550
+ const {notes} = await runnerNotes();
1551
+ expect((await runnerNotes()).notes).toEqual([]);
1552
+ });
1553
+ });
@@ -26,6 +26,11 @@ import {fileURLToPath} from 'node:url';
26
26
  import {spawnSync} from 'node:child_process';
27
27
  import {assertWithin} from '../../foundation/fs/path-safety.mjs';
28
28
  import {resolvePackageDir} from '../../foundation/integrations/integrations.mjs';
29
+ import {
30
+ literalThemeName,
31
+ rootProblem,
32
+ symlinkedFolder,
33
+ } from '../../foundation/integrations/validate-contributions.mjs';
29
34
  import {
30
35
  componentReplacesCliProblem,
31
36
  docsTreeCliProblem,
@@ -953,6 +958,10 @@ async function validatePackedThemeExports(
953
958
  );
954
959
  }
955
960
 
961
+ // The name the source gives defineTheme, when the file says it: the
962
+ // build names its files after it.
963
+ const builtName = literalThemeName(source, localTheme.exportName);
964
+
956
965
  const declaredOutputs = declaredThemeBuildOutputs(
957
966
  localIntegration,
958
967
  localTheme,
@@ -1097,7 +1106,15 @@ async function validatePackedThemeExports(
1097
1106
  ? `Theme "${localTheme.slug}" module is not the module \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` writes and does not only wrap it: ${localWrapper.reason}. A module that wraps the built module may only import the theme's own stylesheet and font stylesheet for their side effects and re-export from the built module either everything or only its theme, as ${localTheme.exportName}.`
1098
1107
  : localWrapper && !localWrapper.ok
1099
1108
  ? `Theme "${localTheme.slug}" module re-exports ${localWrapper.reason}, which does not match ${sourceRelative}. Run \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` before packing.`
1100
- : `Theme "${localTheme.slug}" ${output.kind} does not match ${sourceRelative}. Run \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` before packing.`,
1109
+ : (renamedOutputMessage(
1110
+ localTheme,
1111
+ output.kind,
1112
+ output.local,
1113
+ source,
1114
+ builtName,
1115
+ localIntegration.__packageDir,
1116
+ ) ??
1117
+ `Theme "${localTheme.slug}" ${output.kind} does not match ${sourceRelative}. Run \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` before packing.`),
1101
1118
  ),
1102
1119
  );
1103
1120
  }
@@ -1265,7 +1282,17 @@ export async function integrationPackCheck(options = {}) {
1265
1282
  // ── Phase 1: structure validation ──
1266
1283
  const localResult = await validateLocalIntegration(cwd);
1267
1284
  /** @type {Issue[]} */
1268
- const issues = [...localResult.issues];
1285
+ const issues = [
1286
+ ...localResult.issues,
1287
+ // Validate lists these layout findings without counting them. A
1288
+ // package.json npm cannot pack is what fails the pack here, as it did
1289
+ // before they were named; the rest stay warnings, which fail nothing.
1290
+ ...(localResult.found ? (localResult.listed ?? []) : []).map(issue =>
1291
+ issue.code === 'invalid_package_json'
1292
+ ? {...issue, severity: /** @type {const} */ ('error')}
1293
+ : issue,
1294
+ ),
1295
+ ];
1269
1296
 
1270
1297
  if (!localResult.found || !localResult.integration) {
1271
1298
  return emptyReceipt(pkgName, [
@@ -1314,6 +1341,8 @@ export async function integrationPackCheck(options = {}) {
1314
1341
  );
1315
1342
  }
1316
1343
  for (const root of fileInv.roots) {
1344
+ // A root that is not a readable folder has its own issue.
1345
+ if (rootProblem(/** @type {string} */ (loaded[root.kind]))) continue;
1317
1346
  if (root.files.length === 0) {
1318
1347
  issues.push(
1319
1348
  error(
@@ -1403,6 +1432,9 @@ export async function integrationPackCheck(options = {}) {
1403
1432
  );
1404
1433
  /** @type {string|null} */
1405
1434
  let scratchBase = null;
1435
+ /** @type {string|undefined} the packed copy of the package */
1436
+ let extractDir;
1437
+ const issuesBeforePack = issues.length;
1406
1438
 
1407
1439
  try {
1408
1440
  // npm pack
@@ -1451,11 +1483,16 @@ export async function integrationPackCheck(options = {}) {
1451
1483
  for (const root of fileInv.roots) {
1452
1484
  const missing = root.files.filter(f => !packResult.packedPaths.has(f));
1453
1485
  if (missing.length === root.files.length && root.files.length > 0) {
1486
+ const absolute = /** @type {string} */ (loaded[root.kind]);
1487
+ const link = symlinkedFolder(absolute, packageDir);
1454
1488
  issues.push(
1455
1489
  error(
1456
1490
  'root_not_packed',
1457
1491
  `Declared ${root.kind} root "${root.path}" has 0 of ${root.files.length} expected ` +
1458
- `files in the pack list. Add "${root.path.replace(/^\.\//, '')}" to "files" in package.json.`,
1492
+ `files in the pack list. ` +
1493
+ (link
1494
+ ? `It is reached through the symlink "${path.relative(packageDir, link.path).split(path.sep).join('/')}", and npm pack does not follow a symlinked folder. Fix: replace the link with the folder itself, or point "${root.kind}" at the folder it links to.`
1495
+ : `Add "${root.path.replace(/^\.\//, '')}" to "files" in package.json.`),
1459
1496
  ),
1460
1497
  );
1461
1498
  } else {
@@ -1494,7 +1531,6 @@ export async function integrationPackCheck(options = {}) {
1494
1531
  }
1495
1532
 
1496
1533
  // ── Phase 3: extract + identity comparison ──
1497
- let extractDir;
1498
1534
  try {
1499
1535
  extractDir = resolvePackageDir(pkgName, scratchBase);
1500
1536
  } catch (err) {
@@ -1616,11 +1652,94 @@ export async function integrationPackCheck(options = {}) {
1616
1652
  issues,
1617
1653
  );
1618
1654
  } finally {
1655
+ // The packed copy lives in a scratch folder that is gone once this
1656
+ // returns: name its files the way the package does.
1657
+ if (extractDir && scratchBase) {
1658
+ packageRelative(issues.slice(issuesBeforePack), extractDir, scratchBase);
1659
+ }
1619
1660
  removeTreeBestEffort(tgzTmpDir, 'temporary tarball directory', issues);
1620
1661
  removeTreeBestEffort(scratchBase, 'scratch consumer', issues);
1621
1662
  }
1622
1663
  }
1623
1664
 
1665
+ /**
1666
+ * When a theme's source names it something other than the file the package
1667
+ * exports for it, a rebuild writes new files under that name and leaves the
1668
+ * exported one stale forever: say so, with both ways out. Null otherwise.
1669
+ * @param {{slug: string}} theme
1670
+ * @param {string} kind 'module' or 'stylesheet'
1671
+ * @param {string} exported the file the package exports for it
1672
+ * @param {string} source the theme's source
1673
+ * @param {string | null} builtName the literal defineTheme name, if any
1674
+ * @param {string} packageDir
1675
+ * @returns {string | null}
1676
+ */
1677
+ function renamedOutputMessage(
1678
+ theme,
1679
+ kind,
1680
+ exported,
1681
+ source,
1682
+ builtName,
1683
+ packageDir,
1684
+ ) {
1685
+ if (builtName == null || builtName === theme.slug) return null;
1686
+ const extension = kind === 'module' ? '.js' : '.css';
1687
+ if (path.basename(exported) === `${builtName}${extension}`) return null;
1688
+ const rel = (/** @type {string} */ file) =>
1689
+ path.relative(packageDir, file).split(path.sep).join('/');
1690
+ const built = rel(
1691
+ path.join(path.dirname(source), `${builtName}${extension}`),
1692
+ );
1693
+ return `Theme "${theme.slug}" ${kind} is ${rel(exported)}, but ${path.basename(source)} calls defineTheme with name: '${builtName}', so \`${formatCliCommand(`theme build ${rel(source)}`, packageDir)}\` writes ${built} instead and rebuilding never updates ${path.basename(exported)}. Fix: point the package export that serves it at ${built} after building, or set name: '${theme.slug}' in ${path.basename(source)} and rebuild.`;
1694
+ }
1695
+
1696
+ /**
1697
+ * Rewrite the scratch copy's absolute paths in each issue's message: a file
1698
+ * of the packed package as its path in the package, and any other path in the
1699
+ * scratch consumer (such as the module that resolved an import) as that
1700
+ * consumer. Both folders are gone once verify returns.
1701
+ * @param {Issue[]} issues
1702
+ * @param {string} packedDir the packed package inside the scratch consumer
1703
+ * @param {string} consumerDir the scratch consumer
1704
+ */
1705
+ function packageRelative(issues, packedDir, consumerDir) {
1706
+ /** @param {string} dir */
1707
+ const spellings = dir => {
1708
+ const out = [dir];
1709
+ try {
1710
+ const real = fs.realpathSync(dir);
1711
+ if (real !== dir) out.push(real);
1712
+ } catch {
1713
+ // Already removed: the plain path is all there is.
1714
+ }
1715
+ return out;
1716
+ };
1717
+ /** @param {string} text */
1718
+ const escape = text => text.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
1719
+ const packed = spellings(packedDir);
1720
+ const consumer = spellings(consumerDir);
1721
+ for (const issue of issues) {
1722
+ if (typeof issue.message !== 'string') continue;
1723
+ let message = issue.message;
1724
+ for (const prefix of packed) {
1725
+ message = message
1726
+ .split(`${prefix}${path.sep}`)
1727
+ .join('')
1728
+ .replace(
1729
+ new RegExp(`${escape(prefix)}(?![\\w-]|\\.\\w)`, 'gu'),
1730
+ 'the package root',
1731
+ );
1732
+ }
1733
+ for (const prefix of consumer) {
1734
+ message = message.replace(
1735
+ new RegExp(`${escape(prefix)}[^\\s"'\`]*`, 'gu'),
1736
+ 'a consumer project',
1737
+ );
1738
+ }
1739
+ issue.message = message;
1740
+ }
1741
+ }
1742
+
1624
1743
  /**
1625
1744
  * Assemble the receipt.
1626
1745
  * @param {string} name
@@ -580,6 +580,24 @@ describe('a theme module that loads its own styles', () => {
580
580
  expect(cli.status, cli.stderr).toBe(0);
581
581
  });
582
582
 
583
+ it('says a rebuild cannot fix exports that name the theme after its folder once its source renames it', async () => {
584
+ writePackage({
585
+ files: ['astryx.integration.mjs', 'themes'],
586
+ themeExports: true,
587
+ });
588
+ fs.writeFileSync(
589
+ path.join(tmpDir, 'themes', 'ocean', 'oceanTheme.ts'),
590
+ "import {defineTheme} from '@astryxdesign/core/theme';\n\nexport const oceanTheme = defineTheme({name: 'sea'});\n",
591
+ );
592
+
593
+ const result = await integrationPackCheck({cwd: tmpDir});
594
+
595
+ expect(result.data.issues.map(issue => issue.message)).toEqual([
596
+ `Theme "ocean" module is themes/ocean/ocean.js, but oceanTheme.ts calls defineTheme with name: 'sea', so \`${formatCliCommand('theme build themes/ocean/oceanTheme.ts', tmpDir)}\` writes themes/ocean/sea.js instead and rebuilding never updates ocean.js. Fix: point the package export that serves it at themes/ocean/sea.js after building, or set name: 'ocean' in oceanTheme.ts and rebuild.`,
597
+ `Theme "ocean" stylesheet is themes/ocean/ocean.css, but oceanTheme.ts calls defineTheme with name: 'sea', so \`${formatCliCommand('theme build themes/ocean/oceanTheme.ts', tmpDir)}\` writes themes/ocean/sea.css instead and rebuilding never updates ocean.css. Fix: point the package export that serves it at themes/ocean/sea.css after building, or set name: 'ocean' in oceanTheme.ts and rebuild.`,
598
+ ]);
599
+ });
600
+
583
601
  it('compares a partial export set with the module named after the theme', async () => {
584
602
  writeRenamedOcean();
585
603
  setThemeExports({'./themes/ocean': './themes/ocean/deep-sea.js'});
@@ -1919,6 +1937,75 @@ describe('integrationPackCheck', () => {
1919
1937
  expect(sourceWarnings[0].message).toContain('stray.tsx');
1920
1938
  });
1921
1939
 
1940
+ it('says npm pack skips a symlinked root instead of asking for a "files" entry', async () => {
1941
+ writePackage({
1942
+ manifest:
1943
+ "export default {themes: './themes', components: './components'};\n",
1944
+ });
1945
+ const real = path.join(tmpDir, 'real-components');
1946
+ fs.mkdirSync(real, {recursive: true});
1947
+ fs.writeFileSync(
1948
+ path.join(real, 'Card.doc.mjs'),
1949
+ "export default {type: 'component', name: 'Card', displayName: 'Card', usage: {description: 'Card.'}, props: []};\n",
1950
+ );
1951
+ fs.writeFileSync(
1952
+ path.join(real, 'Card.tsx'),
1953
+ 'export function Card() { return null; }\n',
1954
+ );
1955
+ fs.symlinkSync('real-components', path.join(tmpDir, 'components'));
1956
+
1957
+ const result = await integrationPackCheck({cwd: tmpDir});
1958
+ const codes = result.data.issues.map(issue => issue.code);
1959
+ expect(codes).toContain('symlinked_root');
1960
+ const notPacked = result.data.issues.find(
1961
+ issue => issue.code === 'root_not_packed',
1962
+ );
1963
+ expect(notPacked?.message).toBe(
1964
+ 'Declared components root "components" has 0 of 2 expected files in the pack list. It is reached through the symlink "components", and npm pack does not follow a symlinked folder. Fix: replace the link with the folder itself, or point "components" at the folder it links to.',
1965
+ );
1966
+ });
1967
+
1968
+ it('names packed files by their path in the package, never the scratch copy', async () => {
1969
+ writePackage({
1970
+ manifest:
1971
+ "export default {themes: './themes', docs: './docs/a.doc.mjs'};\n",
1972
+ });
1973
+ fs.mkdirSync(path.join(tmpDir, 'docs'), {recursive: true});
1974
+ fs.writeFileSync(path.join(tmpDir, 'docs', 'a.doc.mjs'), '');
1975
+
1976
+ const result = await integrationPackCheck({cwd: tmpDir});
1977
+ const messages = result.data.issues.map(issue => issue.message);
1978
+ expect(messages.join('\n')).not.toContain(
1979
+ path.join(tmpDir, '.astryx-pack-check-'),
1980
+ );
1981
+ expect(messages).toContain(
1982
+ 'Packed: Declared docs root is a file, not a folder: docs/a.doc.mjs. Fix: point "docs" in the manifest at the folder that holds them.',
1983
+ );
1984
+ // The root's own issue covers it: no empty-root error, no raw errno.
1985
+ const codes = result.data.issues.map(issue => issue.code);
1986
+ expect(codes).not.toContain('empty_contribution_root');
1987
+ expect(codes).not.toContain('local_discovery_failed');
1988
+ expect(codes).not.toContain('packed_discovery_failed');
1989
+ });
1990
+
1991
+ it('fails on a package.json npm cannot pack, which validate only warns about', async () => {
1992
+ writePackage({files: ['astryx.integration.mjs', 'themes']});
1993
+ const packageFile = path.join(tmpDir, 'package.json');
1994
+ const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf-8'));
1995
+ delete pkg.name;
1996
+ fs.writeFileSync(packageFile, `${JSON.stringify(pkg, null, 2)}\n`);
1997
+
1998
+ const result = await integrationPackCheck({cwd: tmpDir});
1999
+
2000
+ expect(result.data.packable).toBe(false);
2001
+ expect(
2002
+ result.data.issues.map(issue => [issue.code, issue.severity]),
2003
+ ).toEqual([
2004
+ ['invalid_package_json', 'error'],
2005
+ ['pack_failed', 'error'],
2006
+ ]);
2007
+ });
2008
+
1922
2009
  it('cleans up its scratch consumer when npm pack fails', async () => {
1923
2010
  writePackage({
1924
2011
  files: ['astryx.integration.mjs', 'themes'],