@astryxdesign/cli 0.7.2 → 0.7.3-canary.0bf23bd

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 (337) hide show
  1. package/README.md +104 -71
  2. package/api/build/_adapter.d.mts +58 -0
  3. package/api/build/_adapter.mjs +115 -3
  4. package/api/build/blocks/blocks.d.mts +42 -0
  5. package/api/build/blocks/blocks.mjs +169 -0
  6. package/api/build/blocks/blocks.test.mjs +108 -0
  7. package/api/build/blocks/blocks.weights.json +1 -0
  8. package/api/build/build.mjs +25 -6
  9. package/api/build/kit/answer.d.mts +14 -0
  10. package/api/build/kit/answer.mjs +85 -0
  11. package/api/build/kit/answer.test.mjs +38 -0
  12. package/api/build/kit/kind.d.mts +25 -0
  13. package/api/build/kit/kind.mjs +170 -0
  14. package/api/build/kit/kit.d.mts +2 -4
  15. package/api/build/kit/kit.mjs +8 -398
  16. package/api/build/kit/rank.d.mts +3 -32
  17. package/api/build/kit/rank.mjs +25 -342
  18. package/api/build/kit/rank.test.mjs +2 -1
  19. package/api/build/kit/signals.d.mts +60 -0
  20. package/api/build/kit/signals.mjs +284 -0
  21. package/api/build/kit/start.d.mts +32 -0
  22. package/api/build/kit/start.mjs +163 -0
  23. package/api/build/kit/words.d.mts +72 -0
  24. package/api/build/kit/words.mjs +202 -0
  25. package/api/build/tree.d.mts +20 -0
  26. package/api/build/tree.mjs +52 -0
  27. package/api/build/tree.test.mjs +177 -0
  28. package/api/build/tree.weights.json +1 -0
  29. package/api/component/component.mjs +17 -3
  30. package/api/component/component.test.mjs +15 -0
  31. package/api/component/doc-links.test.mjs +2 -2
  32. package/api/discover/_adapter.mjs +36 -9
  33. package/api/discover/_adapter.test.mjs +42 -0
  34. package/api/docs/_adapter.d.mts +2 -2
  35. package/api/docs/_adapter.mjs +78 -23
  36. package/api/docs/docs-homes.test.mjs +127 -0
  37. package/api/docs/docs.doc.mjs +3 -3
  38. package/api/docs/docs.test.mjs +35 -19
  39. package/api/docs/integration-tree.test.mjs +20 -20
  40. package/api/docs/integrationDocs.test.mjs +1 -1
  41. package/api/docs/namespace-section.test.mjs +6 -6
  42. package/api/docs/reference-blocks.test.mjs +2 -2
  43. package/api/doctor/doctor.mjs +102 -24
  44. package/api/doctor/doctor.test.mjs +37 -12
  45. package/api/hook/hook.mjs +1 -1
  46. package/api/init/init.doc.mjs +20 -6
  47. package/api/init/init.test.mjs +18 -2
  48. package/api/init/init.type.d.mts +3 -3
  49. package/api/init/init.type.mjs +3 -3
  50. package/api/init/remove/remove.mjs +17 -4
  51. package/api/init/remove/remove.test.mjs +114 -2
  52. package/api/init/run/run.mjs +69 -6
  53. package/api/integration/_adapter.d.mts +8 -0
  54. package/api/integration/_adapter.mjs +21 -1
  55. package/api/integration/authoring-checks.mjs +83 -9
  56. package/api/integration/authoring-checks.test.mjs +220 -0
  57. package/api/integration/pack-check.mjs +123 -4
  58. package/api/integration/pack-check.test.mjs +87 -0
  59. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  60. package/api/integration/validate-integration-gaps.test.mjs +1964 -0
  61. package/api/integration/validate-integration.d.mts +11 -2
  62. package/api/integration/validate-integration.mjs +250 -32
  63. package/api/integration/validate-integration.type.d.mts +59 -1
  64. package/api/integration/validate-integration.type.mjs +46 -2
  65. package/api/integration/validateIntegration.doc.mjs +1 -1
  66. package/api/search/ORDERING.md +28 -0
  67. package/api/search/_adapter.d.mts +82 -2
  68. package/api/search/_adapter.mjs +157 -10
  69. package/api/search/_adapter.test.mjs +173 -0
  70. package/api/search/_candidate-score.d.mts +40 -0
  71. package/api/search/_candidate-score.mjs +238 -0
  72. package/api/search/_cards.json +6098 -0
  73. package/api/search/_compound.d.mts +50 -0
  74. package/api/search/_compound.mjs +170 -0
  75. package/api/search/_doc-searcher.d.mts +4 -0
  76. package/api/search/_doc-searcher.mjs +23 -0
  77. package/api/search/_doc-searcher.test.mjs +36 -0
  78. package/api/search/_doc-searcher.weights.json +1 -0
  79. package/api/search/_doc-text.d.mts +30 -0
  80. package/api/search/_doc-text.mjs +87 -0
  81. package/api/search/_entries.d.mts +52 -0
  82. package/api/search/_entries.mjs +214 -0
  83. package/api/search/_features.d.mts +35 -0
  84. package/api/search/_features.mjs +155 -0
  85. package/api/search/_gather-components.d.mts +26 -0
  86. package/api/search/_gather-components.mjs +284 -0
  87. package/api/search/_gather-docs.d.mts +20 -0
  88. package/api/search/_gather-docs.mjs +254 -0
  89. package/api/search/_gather-hooks.d.mts +12 -0
  90. package/api/search/_gather-hooks.mjs +52 -0
  91. package/api/search/_gather-templates.d.mts +25 -0
  92. package/api/search/_gather-templates.mjs +107 -0
  93. package/api/search/_lexical.d.mts +135 -0
  94. package/api/search/_lexical.mjs +351 -0
  95. package/api/search/_main-searcher.d.mts +19 -0
  96. package/api/search/_main-searcher.mjs +115 -0
  97. package/api/search/_main-searcher.test.mjs +106 -0
  98. package/api/search/_main-searcher.weights.json +1 -0
  99. package/api/search/_module-doc.d.mts +32 -0
  100. package/api/search/_module-doc.mjs +78 -0
  101. package/api/search/_order.d.mts +32 -0
  102. package/api/search/_order.mjs +162 -0
  103. package/api/search/_order.test.mjs +72 -0
  104. package/api/search/_priority.d.mts +50 -0
  105. package/api/search/_priority.mjs +139 -0
  106. package/api/search/_query-words.d.mts +25 -0
  107. package/api/search/_query-words.mjs +92 -0
  108. package/api/search/_query.d.mts +65 -0
  109. package/api/search/_query.mjs +176 -0
  110. package/api/search/_result.d.mts +97 -0
  111. package/api/search/_result.mjs +150 -0
  112. package/api/search/_score.d.mts +60 -0
  113. package/api/search/_score.mjs +283 -0
  114. package/api/search/_synonyms.d.mts +21 -0
  115. package/api/search/_synonyms.mjs +67 -0
  116. package/api/search/_template-searcher.d.mts +4 -0
  117. package/api/search/_template-searcher.mjs +23 -0
  118. package/api/search/_template-searcher.test.mjs +40 -0
  119. package/api/search/_template-searcher.weights.json +1 -0
  120. package/api/search/_titles.d.mts +33 -0
  121. package/api/search/_titles.mjs +65 -0
  122. package/api/search/_words.d.mts +64 -0
  123. package/api/search/_words.mjs +157 -0
  124. package/api/search/search.d.mts +22 -305
  125. package/api/search/search.doc.mjs +8 -6
  126. package/api/search/search.mjs +56 -2482
  127. package/api/search/search.test.mjs +14 -14
  128. package/api/search/search.type.d.mts +6 -1
  129. package/api/search/search.type.mjs +6 -1
  130. package/api/search/tree.d.mts +15 -0
  131. package/api/search/tree.mjs +40 -0
  132. package/api/search/tree.test.mjs +66 -0
  133. package/api/search/tree.weights.json +1 -0
  134. package/api/theme/build/build.packed-old-core.test.mjs +1 -0
  135. package/api/upgrade/_adapter.d.mts +57 -9
  136. package/api/upgrade/_adapter.mjs +198 -28
  137. package/api/upgrade/registry/registry.d.mts +10 -4
  138. package/api/upgrade/registry/registry.mjs +33 -16
  139. package/api/upgrade/registry/registry.test.mjs +85 -0
  140. package/api/upgrade/run/run.d.mts +4 -2
  141. package/api/upgrade/run/run.mjs +13 -2
  142. package/api/upgrade/run/run.test.mjs +417 -3
  143. package/api/upgrade/upgrade.doc.mjs +10 -5
  144. package/api/upgrade/upgrade.mjs +13 -2
  145. package/api/upgrade/upgrade.test.mjs +194 -0
  146. package/api/upgrade/upgrade.type.d.mts +5 -3
  147. package/api/upgrade/upgrade.type.mjs +8 -6
  148. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  149. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  150. package/assets/docs/author-a-theme.doc.mjs +2 -2
  151. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  152. package/assets/docs/tree/add-a-component.doc.mjs +1 -1
  153. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  154. package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
  155. package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
  156. package/assets/docs/tree/api.doc.mjs +6 -0
  157. package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
  158. package/assets/docs/tree/block-template.doc.mjs +1 -1
  159. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  160. package/assets/docs/tree/building-blocks.doc.mjs +2 -2
  161. package/assets/docs/tree/check-the-props.doc.mjs +1 -1
  162. package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
  163. package/assets/docs/tree/checks.doc.mjs +2 -2
  164. package/assets/docs/tree/cli-command.doc.mjs +1 -1
  165. package/assets/docs/tree/cli.doc.mjs +1 -1
  166. package/assets/docs/tree/codemods.doc.mjs +1 -1
  167. package/assets/docs/tree/component-family.doc.mjs +1 -1
  168. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  169. package/assets/docs/tree/components.doc.mjs +1 -1
  170. package/assets/docs/tree/configuration.doc.mjs +1 -1
  171. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
  172. package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
  173. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  174. package/assets/docs/tree/docs.doc.mjs +1 -1
  175. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  176. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  177. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  178. package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
  179. package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
  180. package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
  181. package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
  182. package/assets/docs/tree/global-integrations.doc.mjs +2 -2
  183. package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
  184. package/assets/docs/tree/help.doc.mjs +2 -2
  185. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  186. package/assets/docs/tree/links.doc.mjs +1 -1
  187. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  188. package/assets/docs/tree/page-template.doc.mjs +1 -1
  189. package/assets/docs/tree/publishing.doc.mjs +1 -1
  190. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  191. package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
  192. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  193. package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
  194. package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
  195. package/assets/docs/tree/ship.doc.mjs +2 -2
  196. package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
  197. package/assets/docs/tree/single-component.doc.mjs +1 -1
  198. package/assets/docs/tree/start-a-template.doc.mjs +1 -1
  199. package/assets/docs/tree/subcomponent.doc.mjs +1 -1
  200. package/assets/docs/tree/template-assets.doc.mjs +1 -1
  201. package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
  202. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  203. package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
  204. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  205. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  206. package/assets/docs/tree/template-styles.doc.mjs +1 -1
  207. package/assets/docs/tree/templates.doc.mjs +1 -1
  208. package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
  209. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  210. package/assets/docs/tree/themes.doc.mjs +1 -1
  211. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  212. package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
  213. package/assets/docs/tree/upgrading.doc.mjs +1 -1
  214. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
  215. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  216. package/assets/docs/tree/versioning.doc.mjs +1 -1
  217. package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
  218. package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
  219. package/assets/docs/use-integrations.doc.mjs +120 -0
  220. package/assets/docs/working-with-ai.doc.mjs +6 -2
  221. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  222. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  223. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  224. package/authoring/debug/parse.d.mts +1 -1
  225. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  226. package/authoring/integration/integration.doc.mjs +1 -1
  227. package/authoring/integration/type.ts +1 -1
  228. package/clients/cli/bin/astryx.mjs +7 -78
  229. package/clients/cli/command-result-coverage.test.mjs +6 -5
  230. package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
  231. package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
  232. package/clients/cli/commands/component.test.mjs +6 -6
  233. package/clients/cli/commands/docs.doc.mjs +2 -2
  234. package/clients/cli/commands/docs.mjs +1 -1
  235. package/clients/cli/commands/docs.test.mjs +14 -14
  236. package/clients/cli/commands/doctor.mjs +30 -0
  237. package/clients/cli/commands/doctor.test.mjs +65 -0
  238. package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
  239. package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
  240. package/clients/cli/commands/init.behavior.test.mjs +230 -1
  241. package/clients/cli/commands/init.doc.mjs +28 -9
  242. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  243. package/clients/cli/commands/init.mjs +2 -2
  244. package/clients/cli/commands/integration-check.test.mjs +48 -0
  245. package/clients/cli/commands/integration.doc.mjs +1 -1
  246. package/clients/cli/commands/manifest.doc.mjs +1 -1
  247. package/clients/cli/commands/manifest.mjs +67 -0
  248. package/clients/cli/commands/outside-project.test.mjs +2 -1
  249. package/clients/cli/commands/postinstall.doc.mjs +23 -0
  250. package/clients/cli/commands/postinstall.mjs +31 -0
  251. package/clients/cli/commands/setup-nudge.test.mjs +61 -0
  252. package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
  253. package/clients/cli/commands/upgrade.doc.mjs +4 -2
  254. package/clients/cli/commands/upgrade.mjs +9 -2
  255. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  256. package/clients/cli/index.mjs +29 -86
  257. package/clients/cli/lib/component-format.mjs +1 -1
  258. package/clients/cli/lib/define-command.mjs +12 -7
  259. package/clients/cli/lib/exit-codes.test.mjs +5 -0
  260. package/clients/cli/lib/manifest.test.mjs +12 -0
  261. package/clients/cli/lib/welcome-box.mjs +50 -0
  262. package/clients/cli/run.mjs +108 -0
  263. package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
  264. package/foundation/agent-docs/agent-doc-state.mjs +35 -8
  265. package/foundation/agent-docs/agent-docs.d.mts +100 -12
  266. package/foundation/agent-docs/agent-docs.mjs +1012 -100
  267. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  268. package/foundation/agent-docs/agent-docs.test.mjs +683 -39
  269. package/foundation/config/project.mjs +20 -22
  270. package/foundation/config/project.test.mjs +56 -0
  271. package/foundation/discovery/authoring-self-docs.d.mts +7 -0
  272. package/foundation/discovery/authoring-self-docs.mjs +16 -4
  273. package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
  274. package/foundation/discovery/authoring-surface.d.mts +1 -1
  275. package/foundation/discovery/authoring-surface.mjs +2 -2
  276. package/foundation/discovery/authoring-surface.test.mjs +1 -1
  277. package/foundation/discovery/cli-self-docs.d.mts +2 -1
  278. package/foundation/discovery/cli-self-docs.mjs +4 -3
  279. package/foundation/discovery/cli-self-docs.test.mjs +2 -2
  280. package/foundation/discovery/component-discovery.d.mts +37 -0
  281. package/foundation/discovery/component-discovery.mjs +80 -5
  282. package/foundation/discovery/component-loader.d.mts +1 -1
  283. package/foundation/discovery/docs-discovery.mjs +14 -1
  284. package/foundation/discovery/module-exports.d.mts +41 -0
  285. package/foundation/discovery/module-exports.mjs +253 -0
  286. package/foundation/discovery/runner-allowlist.d.mts +2 -4
  287. package/foundation/discovery/runner-allowlist.mjs +2 -4
  288. package/foundation/discovery/runner-commands.d.mts +77 -0
  289. package/foundation/discovery/runner-commands.mjs +294 -0
  290. package/foundation/discovery/runner-commands.test.mjs +128 -0
  291. package/foundation/discovery/stray-pointers.test.mjs +1 -1
  292. package/foundation/discovery/template-adapter.mjs +18 -1
  293. package/foundation/discovery/template-needs.mjs +1 -44
  294. package/foundation/discovery/theme-discovery.mjs +147 -3
  295. package/foundation/doc-compiler/doc-loads.test.mjs +33 -9
  296. package/foundation/doc-compiler/read.mjs +59 -1
  297. package/foundation/doc-compiler/read.test.mjs +11 -2
  298. package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
  299. package/foundation/doc-compiler/tree.d.mts +32 -6
  300. package/foundation/doc-compiler/tree.mjs +77 -43
  301. package/foundation/doc-compiler/tree.test.mjs +7 -7
  302. package/foundation/fs/paths.d.mts +70 -8
  303. package/foundation/fs/paths.mjs +556 -38
  304. package/foundation/fs/paths.test.mjs +430 -54
  305. package/foundation/fs/read-record.d.mts +18 -0
  306. package/foundation/fs/read-record.mjs +131 -0
  307. package/foundation/fs/read-record.test.mjs +77 -0
  308. package/foundation/integrations/autolink.d.mts +2 -1
  309. package/foundation/integrations/autolink.mjs +4 -13
  310. package/foundation/integrations/computer-integrations.d.mts +6 -3
  311. package/foundation/integrations/computer-integrations.mjs +38 -9
  312. package/foundation/integrations/contribution-inventory.d.mts +15 -20
  313. package/foundation/integrations/contribution-inventory.mjs +41 -5
  314. package/foundation/integrations/integrations.d.mts +27 -1
  315. package/foundation/integrations/integrations.mjs +61 -2
  316. package/foundation/integrations/validate-contributions.d.mts +115 -0
  317. package/foundation/integrations/validate-contributions.mjs +1168 -13
  318. package/foundation/release/handoff.d.mts +23 -4
  319. package/foundation/release/handoff.mjs +175 -8
  320. package/foundation/release/run-context.mjs +14 -0
  321. package/foundation/response/response-types.doc.mjs +1 -1
  322. package/foundation/route/route.d.mts +285 -0
  323. package/foundation/route/route.mjs +324 -0
  324. package/foundation/route/route.test.mjs +153 -0
  325. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  326. package/foundation/runtime/locked-versions.d.mts +32 -0
  327. package/foundation/runtime/locked-versions.mjs +65 -0
  328. package/foundation/runtime/package-root.d.mts +91 -0
  329. package/foundation/runtime/package-root.mjs +174 -0
  330. package/foundation/runtime/package-set.d.mts +62 -0
  331. package/foundation/runtime/package-set.mjs +130 -0
  332. package/foundation/runtime/package-set.test.mjs +432 -0
  333. package/host/door-conformance.test.mjs +274 -0
  334. package/host/index.d.ts +116 -0
  335. package/host/index.mjs +295 -0
  336. package/host/index.test.mjs +302 -0
  337. package/package.json +14 -9
@@ -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'],
@@ -0,0 +1,111 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file A component doc whose file name differs from its source only in
5
+ * letter case, on a case-insensitive disk (macOS, Windows), where discovery
6
+ * still pairs the two: a warning naming the rename, not an error. The disk is
7
+ * simulated by finding the doc's source case-insensitively.
8
+ */
9
+
10
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+
14
+ vi.mock(
15
+ '../../foundation/discovery/component-discovery.mjs',
16
+ async importOriginal => {
17
+ const original =
18
+ /** @type {typeof import('../../foundation/discovery/component-discovery.mjs')} */ (
19
+ await importOriginal()
20
+ );
21
+ return {
22
+ ...original,
23
+ /** @param {string} docPath */
24
+ integrationSourceForDoc: docPath => {
25
+ const dir = path.dirname(docPath);
26
+ const want = `${path
27
+ .basename(docPath)
28
+ .replace(/\.doc\.(ts|mjs|js)$/u, '')}.tsx`.toLowerCase();
29
+ const found = fs
30
+ .readdirSync(dir)
31
+ .find(name => name.toLowerCase() === want);
32
+ return found ? path.join(dir, found) : null;
33
+ },
34
+ };
35
+ },
36
+ );
37
+
38
+ const {validateIntegration} = await import('./validate-integration.mjs');
39
+
40
+ /** @type {string} */
41
+ let tmpDir;
42
+
43
+ beforeEach(() => {
44
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-case-disk-'));
45
+ });
46
+
47
+ afterEach(() => {
48
+ fs.rmSync(tmpDir, {recursive: true, force: true});
49
+ });
50
+
51
+ describe('a doc whose name differs from its source only in case, on a case-insensitive disk', () => {
52
+ it('lists source_without_component_doc with the rename, without an issue', async () => {
53
+ fs.writeFileSync(
54
+ path.join(tmpDir, 'package.json'),
55
+ JSON.stringify({name: '@acme/kit', version: '1.0.0'}),
56
+ );
57
+ fs.writeFileSync(
58
+ path.join(tmpDir, 'astryx.integration.mjs'),
59
+ "export default {components: './components'};\n",
60
+ );
61
+ fs.mkdirSync(path.join(tmpDir, 'components'));
62
+ fs.writeFileSync(
63
+ path.join(tmpDir, 'components', 'Acmewidget.doc.mjs'),
64
+ "export default {type: 'component', name: 'AcmeWidget', displayName: 'AcmeWidget', usage: {description: 'A widget.'}, props: []};\n",
65
+ );
66
+ fs.writeFileSync(
67
+ path.join(tmpDir, 'components', 'AcmeWidget.tsx'),
68
+ 'export function AcmeWidget() { return null; }\n',
69
+ );
70
+
71
+ const {data} = await validateIntegration(undefined, {cwd: tmpDir});
72
+ expect(data.issues).toEqual([]);
73
+ expect(data.notes).toEqual([
74
+ {
75
+ code: 'source_without_component_doc',
76
+ message:
77
+ 'Component doc "Acmewidget.doc.mjs" and its source "AcmeWidget.tsx" differ only in letter case. They pair on a case-insensitive disk (macOS, Windows) but not on Linux, where CI and most installs run, so there the component is missing. Fix: rename Acmewidget.doc.mjs to AcmeWidget.doc.mjs.',
78
+ },
79
+ ]);
80
+ });
81
+
82
+ it('lists a hidden doc that pairs with its source, without an issue', async () => {
83
+ fs.writeFileSync(
84
+ path.join(tmpDir, 'package.json'),
85
+ JSON.stringify({name: '@acme/kit', version: '1.0.0'}),
86
+ );
87
+ fs.writeFileSync(
88
+ path.join(tmpDir, 'astryx.integration.mjs'),
89
+ "export default {components: './components'};\n",
90
+ );
91
+ fs.mkdirSync(path.join(tmpDir, 'components'));
92
+ fs.writeFileSync(
93
+ path.join(tmpDir, 'components', 'Acmewidget.doc.mjs'),
94
+ "export default {type: 'component',\n hidden: true,\n name: 'AcmeWidget', displayName: 'AcmeWidget', usage: {description: 'A widget.'}, props: []};\n",
95
+ );
96
+ fs.writeFileSync(
97
+ path.join(tmpDir, 'components', 'AcmeWidget.tsx'),
98
+ 'export function AcmeWidget() { return null; }\n',
99
+ );
100
+
101
+ const {data} = await validateIntegration(undefined, {cwd: tmpDir});
102
+ expect(data.issues).toEqual([]);
103
+ expect(data.notes).toEqual([
104
+ {
105
+ code: 'source_without_component_doc',
106
+ message:
107
+ 'Hidden component doc "Acmewidget.doc.mjs" and its source "AcmeWidget.tsx" differ only in letter case. They pair on a case-insensitive disk (macOS, Windows) but not on Linux, where CI and most installs run. Fix: rename Acmewidget.doc.mjs to AcmeWidget.doc.mjs.',
108
+ },
109
+ ]);
110
+ });
111
+ });