@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
@@ -0,0 +1,128 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {fileURLToPath} from 'node:url';
7
+ import {describe, expect, it} from 'vitest';
8
+
9
+ import {
10
+ CLI_COMMANDS,
11
+ findRunnerCommands,
12
+ runnerCommandsIn,
13
+ } from './runner-commands.mjs';
14
+
15
+ const COMMAND_DOCS = path.resolve(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '../../clients/cli/commands',
18
+ );
19
+
20
+ describe('runnerCommandsIn', () => {
21
+ it('finds an Astryx command after a project runner, with the bare command', () => {
22
+ expect(runnerCommandsIn('Run `pnpm exec astryx docs theme`.')).toEqual([
23
+ {written: 'pnpm exec astryx docs', bare: 'astryx docs', download: false},
24
+ ]);
25
+ expect(runnerCommandsIn('yarn astryx component Button')).toEqual([
26
+ {
27
+ written: 'yarn astryx component',
28
+ bare: 'astryx component',
29
+ download: false,
30
+ },
31
+ ]);
32
+ });
33
+
34
+ it('always flags the unscoped name after a download runner, even alone', () => {
35
+ expect(runnerCommandsIn('npx astryx doctor; or `bunx astryx`')).toEqual([
36
+ {written: 'npx astryx doctor', bare: 'astryx doctor', download: true},
37
+ {written: 'bunx astryx', bare: 'astryx', download: true},
38
+ ]);
39
+ });
40
+
41
+ it('leaves the scoped package, a project runner named alone, and other tools', () => {
42
+ expect(
43
+ runnerCommandsIn(
44
+ 'Install with `npx @astryxdesign/cli init`. Run it through Yarn (`yarn astryx`). Format with `npx prettier --write`. `npx astryx-tools init`.',
45
+ ),
46
+ ).toEqual([]);
47
+ });
48
+
49
+ it('flags `bun x`, a versioned name, and a name that ends a sentence', () => {
50
+ expect(
51
+ runnerCommandsIn(
52
+ 'Run `bun x astryx doctor`, `npx astryx@latest init` or `npx astryx@0.7.1 docs`. Or just npx astryx.',
53
+ ),
54
+ ).toEqual([
55
+ {written: 'bun x astryx doctor', bare: 'astryx doctor', download: true},
56
+ {
57
+ written: 'npx astryx@latest init',
58
+ bare: 'astryx init',
59
+ download: true,
60
+ },
61
+ {written: 'npx astryx@0.7.1 docs', bare: 'astryx docs', download: true},
62
+ {written: 'npx astryx', bare: 'astryx', download: true},
63
+ ]);
64
+ expect(
65
+ runnerCommandsIn('Edit astryx.config, not npx astryx.config.'),
66
+ ).toEqual([]);
67
+ });
68
+
69
+ it('reads a runner and its command within one line, and needs a CLI command after a project runner', () => {
70
+ expect(
71
+ runnerCommandsIn(
72
+ 'Through Yarn, yarn astryx runs the copy in node_modules.\n```\npnpm astryx\nyarn astryx\n```',
73
+ ),
74
+ ).toEqual([]);
75
+ expect(runnerCommandsIn('npx astryx runs an unrelated package')).toEqual([
76
+ {written: 'npx astryx', bare: 'astryx', download: true},
77
+ ]);
78
+ expect(runnerCommandsIn('pnpm\nastryx docs')).toEqual([]);
79
+ expect(
80
+ runnerCommandsIn('Run `npx -y astryx doctor` or `npx --yes astryx`.'),
81
+ ).toEqual([
82
+ {written: 'npx -y astryx doctor', bare: 'astryx doctor', download: true},
83
+ {written: 'npx --yes astryx', bare: 'astryx', download: true},
84
+ ]);
85
+ });
86
+ });
87
+
88
+ describe('findRunnerCommands', () => {
89
+ it('reads a topic that refers back to itself once, without crashing', async () => {
90
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'runner-self-'));
91
+ const file = path.join(dir, 'acme-setup.doc.mjs');
92
+ fs.writeFileSync(
93
+ file,
94
+ [
95
+ 'const section = {',
96
+ " title: 'Overview',",
97
+ " content: [{type: 'prose', text: 'Run `yarn astryx docs` first.'}],",
98
+ '};',
99
+ "const doc = {type: 'generic', name: 'acme-setup', sections: [section]};",
100
+ 'section.self = section;',
101
+ 'doc.again = doc;',
102
+ 'export default doc;',
103
+ '',
104
+ ].join('\n'),
105
+ );
106
+
107
+ const found = await findRunnerCommands({
108
+ records: [{name: 'acme-setup', path: file}],
109
+ });
110
+
111
+ expect(
112
+ found.map(({command, section, line}) => ({command, section, line})),
113
+ ).toEqual([{command: 'yarn astryx docs', section: 'Overview', line: 3}]);
114
+ });
115
+ });
116
+
117
+ describe('CLI_COMMANDS', () => {
118
+ it('names every top-level command the CLI documents, and nothing else', () => {
119
+ const documented = fs
120
+ .readdirSync(COMMAND_DOCS)
121
+ .filter(file => file.endsWith('.doc.mjs'))
122
+ .map(file => file.slice(0, -'.doc.mjs'.length))
123
+ .filter(name => !name.includes('-') || name === 'gap-report');
124
+ expect([...CLI_COMMANDS].sort()).toEqual(
125
+ [...new Set([...documented, 'help'])].sort(),
126
+ );
127
+ });
128
+ });
@@ -16,7 +16,7 @@ describe('pointersIn', () => {
16
16
  it('leaves a walkthrough alone: more words, a flag, or a route', () => {
17
17
  expect(
18
18
  pointersIn(
19
- 'Run `astryx docs theme overview`, `astryx docs --json`, `astryx docs cli/integrations`, or `astryx docs`.',
19
+ 'Run `astryx docs theme overview`, `astryx docs --json`, `astryx docs authoring/quick-start`, or `astryx docs`.',
20
20
  ),
21
21
  ).toEqual([]);
22
22
  });
@@ -1192,6 +1192,23 @@ function reportSamePackageReplacements(pkg, autolinked, templates, errors) {
1192
1192
  }
1193
1193
  }
1194
1194
 
1195
+ /**
1196
+ * The fix for a template doc with no same-stem `.tsx`: rename a same-stem
1197
+ * source written in another extension, or add the `.tsx`.
1198
+ * @param {string} docPath
1199
+ * @param {string} sourcePath the `.tsx` it needs
1200
+ */
1201
+ function missingTemplateSourceFix(docPath, sourcePath) {
1202
+ const source = path.basename(sourcePath);
1203
+ const stem = source.slice(0, -'.tsx'.length);
1204
+ const other = ['.jsx', '.js', '.ts', '.mjs']
1205
+ .map(extension => `${stem}${extension}`)
1206
+ .find(file => fs.existsSync(path.join(path.dirname(sourcePath), file)));
1207
+ return other
1208
+ ? `Found ${other}, but a template source must be a .tsx file. Fix: rename ${other} to ${source}.`
1209
+ : `Fix: add ${source} beside ${path.basename(docPath)}, with the template as its default export.`;
1210
+ }
1211
+
1195
1212
  /**
1196
1213
  * Discover the templates contributed by a SINGLE integration. Same per-template
1197
1214
  * rules as {@link discoverIntegrationTemplates} (same-stem source required,
@@ -1225,7 +1242,7 @@ export async function discoverIntegrationTemplatesForOne(integration) {
1225
1242
  errors.push({
1226
1243
  package: pkgLabel,
1227
1244
  template: id,
1228
- message: `Template "${id}" is missing its same-stem source file ${path.basename(sourcePath)}.`,
1245
+ message: `Template "${id}" is missing its same-stem source file ${path.basename(sourcePath)}. ${missingTemplateSourceFix(docPath, sourcePath)}`,
1229
1246
  });
1230
1247
  // A replacement this unusable template declares still counts, so its
1231
1248
  // target fails closed instead of going to a sibling.
@@ -18,7 +18,7 @@
18
18
  import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
  import module from 'node:module';
21
- import {CLI_ROOT} from '../fs/paths.mjs';
21
+ import {CLI_ROOT, findWorkspaceRoot} from '../fs/paths.mjs';
22
22
  import {detectStylingSystem} from '../agent-docs/agent-docs.mjs';
23
23
  import {
24
24
  decidePackageManagerIn,
@@ -71,49 +71,6 @@ const ADD_VERB = {
71
71
  npm: 'npm install',
72
72
  };
73
73
 
74
- /**
75
- * Whether a package.json declares a workspace (`workspaces` field).
76
- * @param {string} dir
77
- * @returns {{exists: boolean, declaresWorkspace: boolean}}
78
- */
79
- function readPackageJson(dir) {
80
- const pkgPath = path.join(dir, 'package.json');
81
- if (!fs.existsSync(pkgPath)) return {exists: false, declaresWorkspace: false};
82
- try {
83
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
84
- return {exists: true, declaresWorkspace: pkg.workspaces != null};
85
- } catch {
86
- return {exists: true, declaresWorkspace: false};
87
- }
88
- }
89
-
90
- /**
91
- * The workspace root (AST-048 FR15): the nearest directory at or above the
92
- * target that declares a workspace, by `pnpm-workspace.yaml` or a package.json
93
- * `workspaces` field; outside a workspace, the nearest package.json's
94
- * directory; with no package.json, the target itself.
95
- * @param {string} target
96
- * @returns {string}
97
- */
98
- function findWorkspaceRoot(target) {
99
- let dir = path.resolve(target);
100
- /** @type {string | null} */
101
- let nearestPackage = null;
102
- for (;;) {
103
- const pkg = readPackageJson(dir);
104
- if (
105
- fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) ||
106
- pkg.declaresWorkspace
107
- ) {
108
- return dir;
109
- }
110
- if (pkg.exists && nearestPackage == null) nearestPackage = dir;
111
- const parent = path.dirname(dir);
112
- if (parent === dir) return nearestPackage ?? path.resolve(target);
113
- dir = parent;
114
- }
115
- }
116
-
117
74
  /**
118
75
  * The project's package manager for an install command (AST-048 FR17, FR19):
119
76
  * the nearest directory from the target up to the workspace root that has a
@@ -22,6 +22,7 @@ import * as path from 'node:path';
22
22
  import {lowerDoc} from '../doc-compiler/compile.mjs';
23
23
  import {packageSource} from '../doc-compiler/source.mjs';
24
24
  import {CLI_ROOT} from '../fs/paths.mjs';
25
+ import {formatCliCommand} from '../env/package-manager.mjs';
25
26
  import {LOCAL_THEME_ROOT} from '../config/theme-state.mjs';
26
27
  import {assertWithin, PathSafetyError} from '../fs/path-safety.mjs';
27
28
 
@@ -958,6 +959,126 @@ function declaresThemeDoc(ast, exportDefault) {
958
959
  });
959
960
  }
960
961
 
962
+ /**
963
+ * Whether a file is the module `astryx theme build` writes rather than a
964
+ * theme source: plain JavaScript that carries the build's generated header
965
+ * and never calls defineTheme.
966
+ * @param {string} file
967
+ */
968
+ function isBuiltThemeModule(file) {
969
+ if (!/\.m?js$/u.test(file)) return false;
970
+ let text;
971
+ try {
972
+ text = fs.readFileSync(file, 'utf8');
973
+ } catch {
974
+ return false;
975
+ }
976
+ return (
977
+ /@generated by `astryx theme build`/u.test(text) &&
978
+ !/\bdefineTheme\b/u.test(text)
979
+ );
980
+ }
981
+
982
+ /**
983
+ * The fix when a descriptor's stem names both a theme source and the module
984
+ * `astryx theme build` writes beside it (`brand.ts` and its built `brand.js`):
985
+ * the build names its output after the theme, so the source and descriptor
986
+ * take the `<name>Theme` stem instead. Null when no built module is among
987
+ * the matches, or the stem already ends in `Theme`.
988
+ * @param {string} sourceDir
989
+ * @param {string[]} sources same-stem module file names
990
+ * @param {string} doc the descriptor's file name
991
+ * @param {string} exportName the descriptor's stem
992
+ * @returns {string | null}
993
+ */
994
+ function builtThemeSourceFix(sourceDir, sources, doc, exportName) {
995
+ if (exportName.endsWith('Theme')) return null;
996
+ const built = sources.filter(file =>
997
+ isBuiltThemeModule(path.join(sourceDir, file)),
998
+ );
999
+ const source = sources.filter(file => !built.includes(file));
1000
+ if (built.length === 0 || source.length !== 1) return null;
1001
+ const extension = source[0].slice(exportName.length);
1002
+ const stem = `${exportName}Theme`;
1003
+ return `Fix: ${built.map(file => `"${file}"`).join(', ')} is the module \`${formatCliCommand('theme build', sourceDir)}\` writes, which takes the theme's name; rename ${source[0]} to ${stem}${extension} and ${doc} to ${stem}${THEME_DOC_SUFFIX}, export \`${stem}\` from it, and build again.`;
1004
+ }
1005
+
1006
+ /**
1007
+ * Why a descriptor pairs with the built module and the built module lacks
1008
+ * the export the descriptor's stem names (`ocean.doc.mjs` beside a built
1009
+ * `ocean.js` that exports `oceanTheme`): the descriptor is named for the
1010
+ * build's output, not the theme's source. The source is named only when the
1011
+ * built module's `Source:` header, or a source whose defineTheme gives the
1012
+ * theme's name, confirms it; otherwise the message stays generic. Null when
1013
+ * the entry is not a built module.
1014
+ * @param {string} label the descriptor, as messages name it
1015
+ * @param {string} sourceDir
1016
+ * @param {string} entry the file the descriptor pairs with
1017
+ * @param {string} exportName the descriptor's stem
1018
+ * @param {string} doc the descriptor's file name
1019
+ * @param {string} slug the theme's name
1020
+ * @param {string[]} files the theme folder's files
1021
+ * @returns {string | null}
1022
+ */
1023
+ function builtModuleDescriptorMessage(
1024
+ label,
1025
+ sourceDir,
1026
+ entry,
1027
+ exportName,
1028
+ doc,
1029
+ slug,
1030
+ files,
1031
+ ) {
1032
+ if (!isBuiltThemeModule(path.join(sourceDir, entry))) return null;
1033
+ const source = confirmedThemeSource(sourceDir, entry, slug, files);
1034
+ const head = `${label} shares its stem with the built module "${entry}", which does not export "${exportName}": a descriptor must share its stem with the source that calls defineTheme.`;
1035
+ if (source == null) {
1036
+ return `${head} Fix: rename ${doc} to share its stem with the theme's source (the module that calls defineTheme, such as ${exportName}Theme.ts).`;
1037
+ }
1038
+ const stem = source.replace(/\.[^.]+$/u, '');
1039
+ return `${head} The theme's source is "${source}". Fix: rename ${doc} to ${stem}${THEME_DOC_SUFFIX}.`;
1040
+ }
1041
+
1042
+ /**
1043
+ * The theme's source, when something confirms it: the file the built
1044
+ * module's `Source:` header names, if it is in the folder, else the one
1045
+ * module in the folder whose defineTheme gives the theme's name as a
1046
+ * literal. Null otherwise: a `<name>Theme` file alone may be a helper.
1047
+ * @param {string} sourceDir
1048
+ * @param {string} built the built module's file name
1049
+ * @param {string} slug
1050
+ * @param {string[]} files
1051
+ * @returns {string | null}
1052
+ */
1053
+ function confirmedThemeSource(sourceDir, built, slug, files) {
1054
+ const own = files.filter(
1055
+ file =>
1056
+ !file.includes('/') &&
1057
+ file !== built &&
1058
+ THEME_MODULE_EXTENSIONS.some(extension => file.endsWith(extension)),
1059
+ );
1060
+ /** @param {string} file */
1061
+ const read = file => {
1062
+ try {
1063
+ return fs.readFileSync(path.join(sourceDir, file), 'utf8');
1064
+ } catch {
1065
+ return '';
1066
+ }
1067
+ };
1068
+ const header = /^\s*\*?\s*Source:\s*(\S+)\s*$/mu.exec(read(built));
1069
+ if (header) {
1070
+ const named = path.posix.basename(header[1].replace(/\\/gu, '/'));
1071
+ if (own.includes(named)) return named;
1072
+ }
1073
+ const quoted = slug.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
1074
+ const naming = new RegExp(
1075
+ `\\bdefineTheme\\s*\\(\\s*\\{[^}]*\\bname\\s*:\\s*['"\`]${quoted}['"\`]`,
1076
+ 'u',
1077
+ );
1078
+ const matches = own.filter(file => naming.test(read(file)));
1079
+ return matches.length === 1 ? matches[0] : null;
1080
+ }
1081
+
961
1082
  /** A module named for the theme it exports, such as `oceanTheme.ts`. */
962
1083
  const THEME_SOURCE_RE = /^[$A-Z_a-z][$\w]*Theme\.(?:mjs|js|mts|ts|tsx|jsx)$/u;
963
1084
 
@@ -1240,8 +1361,17 @@ export function discoverThemeDirectory(
1240
1361
  : evidence.includes('/')
1241
1362
  ? `; "${evidence}" is in a subfolder`
1242
1363
  : ` beside "${evidence}"`;
1364
+ const themeDoc = formatCliCommand('docs authoring theme-doc', sourceDir);
1365
+ const fix =
1366
+ docs.length > 1
1367
+ ? `Fix: keep the one descriptor that shares its stem with the theme's source and delete the rest.`
1368
+ : evidence.includes('/')
1369
+ ? `Fix: move "${evidence}" up into ${slug}/, beside where its descriptor goes.`
1370
+ : THEME_SOURCE_RE.test(evidence)
1371
+ ? `Fix: add ${evidence.replace(/\.[^.]+$/u, '')}${THEME_DOC_SUFFIX} beside it with type: 'theme' and name: '${slug}'; \`${themeDoc}\` lists its fields.`
1372
+ : `Fix: add a descriptor that shares its stem with the theme's source, with type: 'theme' and name: '${slug}'; \`${themeDoc}\` lists its fields.`;
1243
1373
  throw new Error(
1244
- `${theme} must contain exactly one same-stem .doc.mjs descriptor; found ${docs.length}${detail}.`,
1374
+ `${theme} must contain exactly one same-stem .doc.mjs descriptor; found ${docs.length}${detail}. ${fix}`,
1245
1375
  );
1246
1376
  }
1247
1377
  const nested = descriptors.find(file => file.includes('/'));
@@ -1267,8 +1397,13 @@ export function discoverThemeDirectory(
1267
1397
  extension => `${exportName}${extension}`,
1268
1398
  ).filter(file => listing.files.includes(file));
1269
1399
  if (sources.length !== 1) {
1400
+ const fix =
1401
+ sources.length > 1
1402
+ ? (builtThemeSourceFix(sourceDir, sources, docs[0], exportName) ??
1403
+ `Fix: keep one of them; the theme's source is a single module.`)
1404
+ : `Fix: add ${exportName}.ts beside ${docs[0]}, exporting \`${exportName}\` from defineTheme.`;
1270
1405
  throw new Error(
1271
- `${theme} must contain exactly one same-stem source for ${docs[0]}; found ${sources.length}${sources.length > 1 ? `: ${quoted(sources)}` : ''}.`,
1406
+ `${theme} must contain exactly one same-stem source for ${docs[0]}; found ${sources.length}${sources.length > 1 ? `: ${quoted(sources)}` : ''}. ${fix}`,
1272
1407
  );
1273
1408
  }
1274
1409
 
@@ -1314,7 +1449,16 @@ export function discoverThemeDirectory(
1314
1449
  )
1315
1450
  ) {
1316
1451
  throw new ThemeRuntimeExportError(
1317
- `Theme "${slug}" for ${owner} entry "${entry}" does not export "${exportName}".`,
1452
+ builtModuleDescriptorMessage(
1453
+ label,
1454
+ sourceDir,
1455
+ entry,
1456
+ exportName,
1457
+ docs[0],
1458
+ slug,
1459
+ listing.files,
1460
+ ) ??
1461
+ `Theme "${slug}" for ${owner} entry "${entry}" does not export "${exportName}".`,
1318
1462
  );
1319
1463
  }
1320
1464
  } catch (error) {
@@ -102,7 +102,15 @@ const RUNNERS = {
102
102
  runs: SELF_DOCS,
103
103
  sites: ['import ../../../api/blog/blog.doc.mjs', 'import ./blog.doc.mjs'],
104
104
  },
105
- 'clients/cli/commands/build-theme.mjs': {
105
+ 'clients/cli/commands/manifest.mjs': {
106
+ runs: SELF_DOCS,
107
+ sites: ['import ./manifest.doc.mjs'],
108
+ },
109
+ 'clients/cli/commands/postinstall.mjs': {
110
+ runs: SELF_DOCS,
111
+ sites: ['import ./postinstall.doc.mjs'],
112
+ },
113
+ 'clients/cli/commands/theme.mjs': {
106
114
  runs: `${SELF_DOCS}; and \`astryx theme build\`, a child process per theme`,
107
115
  sites: [
108
116
  'child_process.spawn',
@@ -135,11 +143,11 @@ const RUNNERS = {
135
143
  'import ./build.doc.mjs',
136
144
  ],
137
145
  },
138
- 'clients/cli/commands/component/index.mjs': {
146
+ 'clients/cli/commands/component.mjs': {
139
147
  runs: SELF_DOCS,
140
148
  sites: [
141
- 'import ../../../../api/component/component.doc.mjs',
142
- 'import ../component.doc.mjs',
149
+ 'import ../../../api/component/component.doc.mjs',
150
+ 'import ./component.doc.mjs',
143
151
  ],
144
152
  },
145
153
  'clients/cli/commands/discover.mjs': {
@@ -180,11 +188,11 @@ const RUNNERS = {
180
188
  'import ./gap-report.doc.mjs',
181
189
  ],
182
190
  },
183
- 'clients/cli/commands/hook/index.mjs': {
191
+ 'clients/cli/commands/hook.mjs': {
184
192
  runs: SELF_DOCS,
185
193
  sites: [
186
- 'import ../../../../api/hook/hook.doc.mjs',
187
- 'import ../hook.doc.mjs',
194
+ 'import ../../../api/hook/hook.doc.mjs',
195
+ 'import ./hook.doc.mjs',
188
196
  ],
189
197
  },
190
198
  'clients/cli/commands/init.mjs': {
@@ -233,8 +241,8 @@ const RUNNERS = {
233
241
  ],
234
242
  },
235
243
  'clients/cli/index.mjs': {
236
- runs: 'the command modules, to register each command, and the manifest command doc',
237
- sites: ['import ./commands/manifest.doc.mjs', 'import(<computed>)'],
244
+ runs: 'the command modules, to register each command',
245
+ sites: ['import(<computed>)'],
238
246
  },
239
247
  'clients/cli/lib/manifest.mjs': {
240
248
  runs: 'the manifest command doc, for the exit codes the manifest reports',
@@ -248,6 +256,10 @@ const RUNNERS = {
248
256
  runs: 'astryx.config, through the config parser',
249
257
  sites: ['loadModuleWithParser(..., parseConfig)'],
250
258
  },
259
+ 'foundation/agent-docs/agent-docs.mjs': {
260
+ runs: 'git, sl or hg, to read whether the repository ignores an untracked agent file',
261
+ sites: ['child_process.spawnSync'],
262
+ },
251
263
  'foundation/fs/file-protection.mjs': {
252
264
  runs: 'the ignore matcher (CommonJS), loaded when a resolver first reads an ignore file',
253
265
  sites: ['require(ignore)'],
@@ -272,6 +284,10 @@ const RUNNERS = {
272
284
  runs: 'a contributed topic, for its catalog fields: name, title, description, category, and relationships',
273
285
  sites: ['importDocModule'],
274
286
  },
287
+ 'foundation/discovery/runner-commands.mjs': {
288
+ runs: "the package's topics, to find each Astryx command written with a package runner",
289
+ sites: ['loadTopicModule'],
290
+ },
275
291
  'foundation/discovery/stray-pointers.mjs': {
276
292
  runs: "the CLI's own guides, for their names, to tell which written `astryx docs <name>` points at a doc that exists",
277
293
  sites: ['builtinGuideFiles'],
@@ -315,6 +331,10 @@ const RUNNERS = {
315
331
  'require.cache',
316
332
  ],
317
333
  },
334
+ 'foundation/fs/read-record.mjs': {
335
+ runs: "nothing; it wraps node:fs's read calls to record the paths an operation reads, and republishes them to ESM imports",
336
+ sites: ['module.syncBuiltinESMExports'],
337
+ },
318
338
  'foundation/fs/paths.mjs': {
319
339
  runs: "Yarn's pnpapi, under Plug'n'Play, to find an installed package",
320
340
  sites: ['require(pnpapi)'],
@@ -339,6 +359,10 @@ const RUNNERS = {
339
359
  runs: "the CLI release that answers a project's run, when it is not this copy",
340
360
  sites: ['child_process.spawn'],
341
361
  },
362
+ 'host/index.mjs': {
363
+ runs: "another release's host entry, from one of the host's package sets, when that release answers a hosted run",
364
+ sites: ['import(<computed>)'],
365
+ },
342
366
  'scripts/generate-cli-readme.mjs': {
343
367
  runs: '`astryx manifest` and the error-code and response-type docs, to write the README tables (build time, not shipped)',
344
368
  sites: ['child_process.spawnSync', 'import(<computed>)'],
@@ -173,8 +173,23 @@ export async function readDocView(file, options) {
173
173
  );
174
174
  if (result.loadFailed) throw result.loadFailure;
175
175
  if (result.missing) {
176
- if (strict)
176
+ if (strict) {
177
+ // A doc that exports nothing its reader takes (an empty file, or one
178
+ // with other exports) is said plainly, with what it does export and
179
+ // the fix; the schema would only say "(root): Invalid input".
180
+ const exported =
181
+ options.exports ??
182
+ /** @type {Record<string, readonly string[]>} */ (DOC_EXPORTS)[
183
+ options.root
184
+ ] ??
185
+ [];
186
+ const problem =
187
+ result.missingValue === undefined
188
+ ? await missingDocProblem(file, options, exported)
189
+ : null;
190
+ if (problem) throw new Error(`${options.label ?? file} ${problem}`);
177
191
  parserFor(options.root)(result.missingValue, options.label ?? file);
192
+ }
178
193
  return result.missingValue;
179
194
  }
180
195
  if (strict && result.failed) throw result.failure;
@@ -182,6 +197,49 @@ export async function readDocView(file, options) {
182
197
  return result.view;
183
198
  }
184
199
 
200
+ /**
201
+ * Why a doc file exports no doc, with the fix: it is empty, or it exports
202
+ * other names than the ones its reader takes. Null when neither explains it.
203
+ * @param {string} file
204
+ * @param {ReadOptions} options
205
+ * @param {readonly string[]} exported the export names the reader takes
206
+ * @returns {Promise<string | null>}
207
+ */
208
+ async function missingDocProblem(file, options, exported) {
209
+ // A theme descriptor is read statically, never imported.
210
+ if (options.root === 'themes') return null;
211
+ const fix =
212
+ 'Fix: export the doc object as the default export: `export default {type: ..., name: ..., ...}`.';
213
+ let text;
214
+ try {
215
+ text = fs.readFileSync(file, 'utf8');
216
+ } catch {
217
+ return null;
218
+ }
219
+ const code = text
220
+ .replace(/^\uFEFF/u, '')
221
+ .replace(/\/\*[\s\S]*?\*\//gu, '')
222
+ .replace(/(^|[^:])\/\/.*$/gmu, '$1')
223
+ .trim();
224
+ if (code === '') return `is empty, so it documents nothing. ${fix}`;
225
+ /** @type {string[]} */
226
+ let names;
227
+ try {
228
+ const mod = await LOADERS[options.loader ?? 'user'](file);
229
+ names = Object.keys(mod ?? {}).filter(name => name !== 'default');
230
+ } catch {
231
+ return null;
232
+ }
233
+ const takes = exported.map(name =>
234
+ name === 'default' ? 'default export' : `\`${name}\` export`,
235
+ );
236
+ const found =
237
+ names.length > 0
238
+ ? ` (it exports ${names.map(name => `\`${name}\``).join(', ')})`
239
+ : ' (it exports nothing)';
240
+ return `has no ${takes.join(' or ')}${found}, so it documents nothing. ${fix}`;
241
+ }
242
+
185
243
  /**
186
244
  * Load one file as its root reads it: the doc its module exports and, for a
187
245
  * component or hook, the translation it exports for `lang`.
@@ -109,10 +109,19 @@ describe('readDocView', () => {
109
109
  }
110
110
  });
111
111
 
112
- it("reads a file with no doc as undefined, or as its parser's error when checked", async () => {
112
+ it('reads a file with no doc as undefined, or, checked, says what it exports instead', async () => {
113
113
  const file = write('Empty.doc.mjs', 'export const unrelated = 1;\n');
114
114
  await expect(loadDocs(file)).resolves.toBeUndefined();
115
- await expect(loadComponentDoc(file)).rejects.toThrow(`${file} is invalid`);
115
+ await expect(loadComponentDoc(file)).rejects.toThrow(
116
+ `${file} has no default export or \`docs\` export (it exports \`unrelated\`), so it documents nothing. Fix: export the doc object as the default export`,
117
+ );
118
+ });
119
+
120
+ it('says a checked doc file is empty', async () => {
121
+ const file = write('Blank.doc.mjs', '// nothing yet\n');
122
+ await expect(loadComponentDoc(file)).rejects.toThrow(
123
+ `${file} is empty, so it documents nothing.`,
124
+ );
116
125
  });
117
126
 
118
127
  it('applies the translation a module exports for the reading language', async () => {
@@ -3,7 +3,8 @@
3
3
  /**
4
4
  * @file A topic split into a namespace of guides indexes each term once, a
5
5
  * policy of the docs split itself (no spec clause states it). Below the CLI's own
6
- * root namespaces (every one but `cli`, its command and API reference), a
6
+ * root namespaces (every one but `cli`, its command and API reference, and
7
+ * `authoring`, the integration guides `cli` held before this rule), a
7
8
  * guide's search keywords are its own: none repeats a keyword its namespace,
8
9
  * a sibling, or a guide of another split topic declares. A term several
9
10
  * guides share belongs to their namespace, or to none.
@@ -45,7 +46,7 @@ describe('the keywords of a split topic', () => {
45
46
  return doc?.name;
46
47
  };
47
48
  const roots = [...docs.values()]
48
- .filter(doc => doc.type === 'namespace' && !parentOf(doc) && doc.name !== 'cli')
49
+ .filter(doc => doc.type === 'namespace' && !parentOf(doc) && !['cli', 'authoring'].includes(doc.name))
49
50
  .map(doc => doc.name);
50
51
  expect(roots).toContain('layout');
51
52