@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
@@ -309,6 +309,259 @@ export function moduleExportSource(file, exportName, seen = new Set()) {
309
309
  return found;
310
310
  }
311
311
 
312
+ /**
313
+ * The module's own top-level statements of one type: an `export` inside a
314
+ * TypeScript namespace names a member of that namespace, not of the module.
315
+ * @param {any} program
316
+ * @param {string} type
317
+ * @returns {any[]}
318
+ */
319
+ function topLevel(program, type) {
320
+ return program.body.filter((/** @type {any} */ node) => node.type === type);
321
+ }
322
+
323
+ /**
324
+ * Every runtime name a module exports that this read can see (`default` for
325
+ * a default export), or null when it cannot see them all: an `export *`, `export =`, CommonJS exports, or a
326
+ * file that does not parse.
327
+ * @param {string} file
328
+ * @returns {string[] | null}
329
+ */
330
+ export function moduleExportNames(file) {
331
+ let text;
332
+ let ast;
333
+ try {
334
+ if (!fs.statSync(file).isFile()) return null;
335
+ text = moduleText(file);
336
+ ast = parseModule(file);
337
+ } catch {
338
+ return null;
339
+ }
340
+ if (/\bmodule\.exports\b|\bexports\.[$\w]/u.test(text)) return null;
341
+ if (
342
+ topLevel(ast.program, 'TSExportAssignment').length > 0 ||
343
+ topLevel(ast.program, 'ExportAllDeclaration').length > 0
344
+ ) {
345
+ return null;
346
+ }
347
+ /** @type {Set<string>} */
348
+ const names = new Set();
349
+ for (const node of topLevel(ast.program, 'TSImportEqualsDeclaration')) {
350
+ if (!node.isExport) continue;
351
+ // A type-only alias has no runtime value; any other might.
352
+ if (node.importKind === 'type') continue;
353
+ if (typeof node.id?.name !== 'string') return null;
354
+ names.add(node.id.name);
355
+ }
356
+ for (const node of topLevel(ast.program, 'ExportNamedDeclaration')) {
357
+ if (node.exportKind === 'type') continue;
358
+ const declaration = node.declaration;
359
+ if (declaration?.type === 'VariableDeclaration') {
360
+ for (const item of declaration.declarations) {
361
+ if (item.id?.type !== 'Identifier') return null;
362
+ names.add(item.id.name);
363
+ }
364
+ } else if (declaration != null) {
365
+ if (!runtimeDefault(declaration)) continue;
366
+ if (typeof declaration.id?.name !== 'string') return null;
367
+ names.add(declaration.id.name);
368
+ }
369
+ for (const specifier of node.specifiers ?? []) {
370
+ if (specifier.exportKind === 'type') continue;
371
+ const exported = specifier.exported?.name ?? specifier.exported?.value;
372
+ if (typeof exported === 'string') names.add(exported);
373
+ }
374
+ }
375
+ if (
376
+ topLevel(ast.program, 'ExportDefaultDeclaration').some(
377
+ (/** @type {any} */ node) => runtimeDefault(node.declaration),
378
+ )
379
+ ) {
380
+ names.add('default');
381
+ }
382
+ return [...names].sort();
383
+ }
384
+
385
+ /**
386
+ * @typedef {{verdict: 'exports'} | {verdict: 'lacks'} | {verdict: 'unknown', reason: string}} ExportVerdict
387
+ */
388
+
389
+ /**
390
+ * Whether a module exports `exportName` at runtime, read without executing
391
+ * it: `exports` when a declaration or a followed re-export binds it, `lacks`
392
+ * only when every export the module writes was read and none binds it, and
393
+ * `unknown`, with the first reason, when some export could not be read: a
394
+ * re-export from another package or from a specifier that resolves to no
395
+ * file, a destructured binding, a namespace, an ambient or const enum
396
+ * declaration, `export =`, CommonJS exports, or a file that does not parse.
397
+ * @param {string} file
398
+ * @param {string} exportName
399
+ * @param {Set<string>} [seen]
400
+ * @returns {ExportVerdict}
401
+ */
402
+ export function moduleExportVerdict(file, exportName, seen = new Set()) {
403
+ const identity = `${file}\0${exportName}`;
404
+ // A cycle adds nothing a module's own exports have not already said.
405
+ if (seen.has(identity)) return {verdict: 'lacks'};
406
+ seen.add(identity);
407
+ const shown = path.basename(file);
408
+ let text;
409
+ let ast;
410
+ try {
411
+ if (!fs.statSync(file).isFile()) {
412
+ return {verdict: 'unknown', reason: `${shown} is not a file.`};
413
+ }
414
+ text = moduleText(file);
415
+ ast = parseModule(file);
416
+ } catch {
417
+ return {
418
+ verdict: 'unknown',
419
+ reason: `${shown} could not be read or parsed.`,
420
+ };
421
+ }
422
+ /** @type {string[]} */
423
+ const unknown = [];
424
+ if (/\bmodule\.exports\b|\bexports\.[$\w]/u.test(text)) {
425
+ unknown.push(
426
+ `${shown} uses CommonJS exports, whose names this check does not read.`,
427
+ );
428
+ }
429
+ if (topLevel(ast.program, 'TSExportAssignment').length > 0) {
430
+ unknown.push(`${shown} uses \`export =\`, whose names this check does not read.`);
431
+ }
432
+ /**
433
+ * Follow a re-export of `imported` from `specifier`.
434
+ * @param {string} specifier
435
+ * @param {string} imported
436
+ * @param {string} what how the reason names what is re-exported
437
+ * @returns {boolean} true when it proves the export
438
+ */
439
+ const follow = (specifier, imported, what) => {
440
+ if (!specifier.startsWith('.')) {
441
+ unknown.push(
442
+ `${shown} re-exports ${what} from "${specifier}", another package this check does not read.`,
443
+ );
444
+ return false;
445
+ }
446
+ const target = resolveLocalModule(specifier, file);
447
+ if (!target) {
448
+ unknown.push(
449
+ `${shown} re-exports ${what} from "${specifier}", which resolves to no file this check can read.`,
450
+ );
451
+ return false;
452
+ }
453
+ const inner = moduleExportVerdict(target, imported, seen);
454
+ if (inner.verdict === 'exports') return true;
455
+ if (inner.verdict === 'unknown') unknown.push(inner.reason);
456
+ return false;
457
+ };
458
+ // `export import Name = Some.Path;` binds a runtime value unless it is
459
+ // type-only.
460
+ for (const node of topLevel(ast.program, 'TSImportEqualsDeclaration')) {
461
+ if (!node.isExport || node.id?.name !== exportName) continue;
462
+ if (node.importKind === 'type') {
463
+ unknown.push(
464
+ `${shown} exports ${exportName} with \`export import type\`, which has no runtime value this check can see.`,
465
+ );
466
+ } else {
467
+ return {verdict: 'exports'};
468
+ }
469
+ }
470
+ for (const node of topLevel(ast.program, 'ExportNamedDeclaration')) {
471
+ if (node.exportKind === 'type') continue;
472
+ const declaration = node.declaration;
473
+ if (declaration?.type === 'VariableDeclaration') {
474
+ for (const item of declaration.declarations) {
475
+ if (item.id?.type !== 'Identifier') {
476
+ unknown.push(
477
+ `${shown} exports a destructured binding, whose names this check does not read.`,
478
+ );
479
+ } else if (item.id.name === exportName) {
480
+ if (declaration.declare === true) {
481
+ unknown.push(
482
+ `${shown} declares ${exportName} with \`declare\`, which has no runtime value this check can see.`,
483
+ );
484
+ } else {
485
+ return {verdict: 'exports'};
486
+ }
487
+ }
488
+ }
489
+ } else if (declaration != null && declaration.id?.name === exportName) {
490
+ if (
491
+ ['TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(
492
+ declaration.type,
493
+ )
494
+ ) {
495
+ // A type only: no runtime export.
496
+ } else if (declaration.type === 'TSModuleDeclaration') {
497
+ unknown.push(
498
+ `${shown} exports ${exportName} as a TypeScript namespace, which has a runtime value only when it holds one.`,
499
+ );
500
+ } else if (declaration.declare === true || declaration.const === true) {
501
+ unknown.push(
502
+ `${shown} declares ${exportName} as an ambient or const declaration, which has no runtime value this check can see.`,
503
+ );
504
+ } else if (runtimeDefault(declaration)) {
505
+ return {verdict: 'exports'};
506
+ }
507
+ }
508
+ for (const specifier of node.specifiers ?? []) {
509
+ if (specifier.exportKind === 'type') continue;
510
+ const exported = specifier.exported?.name ?? specifier.exported?.value;
511
+ if (exported !== exportName) continue;
512
+ if (!node.source || specifier.type === 'ExportNamespaceSpecifier') {
513
+ return {verdict: 'exports'};
514
+ }
515
+ if (specifier.type !== 'ExportSpecifier') {
516
+ unknown.push(`${shown} re-exports ${exportName} in a form this check does not read.`);
517
+ continue;
518
+ }
519
+ const imported =
520
+ specifier.local?.name ?? specifier.local?.value ?? exportName;
521
+ if (follow(String(node.source.value), imported, exportName)) {
522
+ return {verdict: 'exports'};
523
+ }
524
+ }
525
+ }
526
+ if (exportName === 'default') {
527
+ const defaults = topLevel(ast.program, 'ExportDefaultDeclaration');
528
+ if (
529
+ defaults.some((/** @type {any} */ node) =>
530
+ runtimeDefault(node.declaration),
531
+ )
532
+ ) {
533
+ return {verdict: 'exports'};
534
+ }
535
+ }
536
+ for (const node of topLevel(ast.program, 'ExportAllDeclaration')) {
537
+ if (node.exportKind === 'type') continue;
538
+ const exported = node.exported?.name ?? node.exported?.value;
539
+ if (exported != null) {
540
+ if (exported === exportName) return {verdict: 'exports'};
541
+ continue;
542
+ }
543
+ // `export *` never re-exports a default.
544
+ if (exportName === 'default') continue;
545
+ if (follow(String(node.source?.value ?? ''), exportName, 'everything')) {
546
+ return {verdict: 'exports'};
547
+ }
548
+ }
549
+ return unknown.length > 0
550
+ ? {verdict: 'unknown', reason: unknown[0]}
551
+ : {verdict: 'lacks'};
552
+ }
553
+
554
+ /**
555
+ * Whether a module provably does not export `exportName` at runtime: see
556
+ * {@link moduleExportVerdict}.
557
+ * @param {string} file
558
+ * @param {string} exportName
559
+ * @returns {boolean}
560
+ */
561
+ export function moduleLacksExport(file, exportName) {
562
+ return moduleExportVerdict(file, exportName).verdict === 'lacks';
563
+ }
564
+
312
565
  /**
313
566
  * Marks an object {@link staticExportObject} returns (or one inside it) whose
314
567
  * source also has properties or spreads it could not read as literals. Not
@@ -14,10 +14,8 @@
14
14
  * command, except in `runnerExamples`, where `npx @astryxdesign/cli …` and a
15
15
  * project's runner may (still never the unscoped name through npx).
16
16
  *
17
- * TODO(doctor-runner-check): the Doctor lane's integration-docs runner check
18
- * (foundation/discovery/runner-commands.mjs) becomes the single rule; then the
19
- * repo test runs that check over Core's and the CLI's own docs with this
20
- * allowlist.
17
+ * The matcher is runnerCommandsIn (foundation/discovery/runner-commands.mjs),
18
+ * the rule `doctor integration validate` holds integrations to.
21
19
  *
22
20
  * @input nothing
23
21
  * @output the allowlist
@@ -13,10 +13,8 @@
13
13
  * command, except in `runnerExamples`, where `npx @astryxdesign/cli …` and a
14
14
  * project's runner may (still never the unscoped name through npx).
15
15
  *
16
- * TODO(doctor-runner-check): the Doctor lane's integration-docs runner check
17
- * (foundation/discovery/runner-commands.mjs) becomes the single rule; then the
18
- * repo test runs that check over Core's and the CLI's own docs with this
19
- * allowlist.
16
+ * The matcher is runnerCommandsIn (foundation/discovery/runner-commands.mjs),
17
+ * the rule `doctor integration validate` holds integrations to.
20
18
  *
21
19
  * @input nothing
22
20
  * @output the allowlist
@@ -0,0 +1,77 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * Each runner command in a text, as written, with the bare command to write
6
+ * instead, in order.
7
+ *
8
+ * @param {string} text
9
+ * @returns {{written: string, bare: string, download: boolean}[]}
10
+ */
11
+ export function runnerCommandsIn(text: string): {
12
+ written: string;
13
+ bare: string;
14
+ download: boolean;
15
+ }[];
16
+ /**
17
+ * An Astryx command a topic writes with a package runner in front.
18
+ *
19
+ * @typedef {object} RunnerCommand
20
+ * @property {string} topic
21
+ * @property {string | null} section the title of the section it is in
22
+ * @property {string} file the doc file
23
+ * @property {number | null} line the file line that holds it, when found
24
+ * @property {string} command the command as written
25
+ * @property {string} message the bare command to write instead
26
+ */
27
+ /**
28
+ * Runner commands in a package's topics.
29
+ *
30
+ * @param {{records: {name: string, path: string}[], guides?: {name: string, ref?: {topicFile?: string}}[]}} discovered
31
+ * the package's docs, as discoverIntegrationDocs returns them
32
+ * @returns {Promise<RunnerCommand[]>}
33
+ */
34
+ export function findRunnerCommands(discovered: {
35
+ records: {
36
+ name: string;
37
+ path: string;
38
+ }[];
39
+ guides?: {
40
+ name: string;
41
+ ref?: {
42
+ topicFile?: string;
43
+ };
44
+ }[];
45
+ }): Promise<RunnerCommand[]>;
46
+ /**
47
+ * The CLI's top-level commands, the words that make a project runner's
48
+ * `astryx <word>` a command; runner-commands.test.mjs keeps the list equal to
49
+ * the command docs.
50
+ */
51
+ export const CLI_COMMANDS: readonly string[];
52
+ /**
53
+ * An Astryx command a topic writes with a package runner in front.
54
+ */
55
+ export type RunnerCommand = {
56
+ topic: string;
57
+ /**
58
+ * the title of the section it is in
59
+ */
60
+ section: string | null;
61
+ /**
62
+ * the doc file
63
+ */
64
+ file: string;
65
+ /**
66
+ * the file line that holds it, when found
67
+ */
68
+ line: number | null;
69
+ /**
70
+ * the command as written
71
+ */
72
+ command: string;
73
+ /**
74
+ * the bare command to write instead
75
+ */
76
+ message: string;
77
+ };
@@ -0,0 +1,294 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Runner commands in docs: an Astryx command written with a package runner in
5
+ * front. Docs write the bare `astryx <command>` and the CLI shows it with the
6
+ * reader's runner, so a written runner is wrong for every reader on another
7
+ * package manager.
8
+ *
9
+ * - A download runner (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`, `bun x`,
10
+ * `npm exec`) with the unscoped name `astryx`, with or without an
11
+ * `@<version>` after it, always counts, even with nothing after it: it
12
+ * fetches an unrelated package of that name.
13
+ * - A project runner (`pnpm exec`, `pnpm`, `yarn`) counts when one of the
14
+ * CLI's commands follows on the same line (`pnpm exec astryx docs`). Named
15
+ * alone, or before a word that is not a command (`yarn astryx runs the
16
+ * copy…`), it is left alone.
17
+ * - A runner and what follows it are read within one line; flags between a
18
+ * download runner and the name are read past.
19
+ * - The scoped package (`npx @astryxdesign/cli init`) never counts: it is the
20
+ * one form that runs before the CLI is installed, for install steps.
21
+ * - Other tools' commands (`npx prettier`) never count.
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+
26
+ import {loadTopicModule} from './docs-discovery.mjs';
27
+
28
+ /**
29
+ * @param {string} file
30
+ * @returns {Promise<any>} the doc, or null for one that does not load (the
31
+ * load problem is reported elsewhere)
32
+ */
33
+ async function loadQuietly(file) {
34
+ try {
35
+ return await loadTopicModule(file);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ const DOWNLOAD_RUNNERS = [
42
+ 'pnpm dlx',
43
+ 'yarn dlx',
44
+ 'npm exec --',
45
+ 'npm exec',
46
+ 'npx',
47
+ 'bunx',
48
+ 'bun x',
49
+ ];
50
+ const PROJECT_RUNNERS = ['pnpm exec', 'pnpm', 'yarn'];
51
+
52
+ /**
53
+ * The CLI's top-level commands, the words that make a project runner's
54
+ * `astryx <word>` a command; runner-commands.test.mjs keeps the list equal to
55
+ * the command docs.
56
+ */
57
+ export const CLI_COMMANDS = Object.freeze([
58
+ 'blog',
59
+ 'build',
60
+ 'component',
61
+ 'discover',
62
+ 'docs',
63
+ 'doctor',
64
+ 'gap-report',
65
+ 'help',
66
+ 'hook',
67
+ 'init',
68
+ 'integration',
69
+ 'manifest',
70
+ 'postinstall',
71
+ 'search',
72
+ 'swizzle',
73
+ 'template',
74
+ 'theme',
75
+ 'upgrade',
76
+ ]);
77
+
78
+ /** Blanks within one line. */
79
+ const BLANK = '[ \\t]+';
80
+
81
+ /** @param {readonly string[]} words */
82
+ const alternatives = words =>
83
+ words.map(each => each.replace(/ /g, BLANK)).join('|');
84
+
85
+ /** After `astryx`: the name ends here (a sentence's period may follow). */
86
+ const NAME_END = '(?![\\w@/-]|\\.\\w)';
87
+
88
+ const DOWNLOAD_COMMAND = new RegExp(
89
+ `(?<![\\w@/.-])(?:${alternatives(DOWNLOAD_RUNNERS)})(?:${BLANK}--?[a-z][\\w-]*)*${BLANK}astryx(?:@[\\w^~-]+(?:\\.[\\w^~-]+)*)?${NAME_END}(?:${BLANK}(${alternatives(CLI_COMMANDS)})(?![\\w-]))?`,
90
+ 'g',
91
+ );
92
+ const PROJECT_COMMAND = new RegExp(
93
+ `(?<![\\w@/.-])(?:${alternatives(PROJECT_RUNNERS)})${BLANK}astryx${BLANK}(${alternatives(CLI_COMMANDS)})(?![\\w-])`,
94
+ 'g',
95
+ );
96
+
97
+ /**
98
+ * Each runner command in a text, as written, with the bare command to write
99
+ * instead, in order.
100
+ *
101
+ * @param {string} text
102
+ * @returns {{written: string, bare: string, download: boolean}[]}
103
+ */
104
+ export function runnerCommandsIn(text) {
105
+ const found = [
106
+ ...[...String(text).matchAll(DOWNLOAD_COMMAND)].map(match => ({
107
+ match,
108
+ download: true,
109
+ })),
110
+ ...[...String(text).matchAll(PROJECT_COMMAND)].map(match => ({
111
+ match,
112
+ download: false,
113
+ })),
114
+ ];
115
+ /** @type {{at: number, written: string, bare: string, download: boolean}[]} */
116
+ const unique = [];
117
+ for (const {match, download} of found) {
118
+ if (unique.some(each => each.at === match.index)) continue;
119
+ unique.push({
120
+ at: match.index,
121
+ written: match[0],
122
+ bare: match[1] ? `astryx ${match[1]}` : 'astryx',
123
+ download,
124
+ });
125
+ }
126
+ return unique
127
+ .sort((a, b) => a.at - b.at)
128
+ .map(({written, bare, download}) => ({written, bare, download}));
129
+ }
130
+
131
+ /**
132
+ * Every string in a doc, with the title of the section it is in. Code counts:
133
+ * most commands sit in code blocks.
134
+ *
135
+ * @param {unknown} doc
136
+ * @returns {{text: string, section: string | null}[]}
137
+ */
138
+ function allStrings(doc) {
139
+ /** @type {{text: string, section: string | null}[]} */
140
+ const strings = [];
141
+ // A doc that refers back to itself is walked once.
142
+ const visited = new WeakSet();
143
+ /**
144
+ * @param {unknown} value
145
+ * @param {string | null} section
146
+ */
147
+ const walk = (value, section) => {
148
+ if (typeof value === 'string') {
149
+ strings.push({text: value, section});
150
+ } else if (value != null && typeof value === 'object') {
151
+ if (visited.has(value)) return;
152
+ visited.add(value);
153
+ if (Array.isArray(value)) {
154
+ for (const item of value) walk(item, section);
155
+ return;
156
+ }
157
+ const record = /** @type {Record<string, unknown>} */ (value);
158
+ for (const [key, field] of Object.entries(record)) {
159
+ if (key === 'sections' && Array.isArray(field)) {
160
+ for (const each of field) {
161
+ walk(each, typeof each?.title === 'string' ? each.title : section);
162
+ }
163
+ } else {
164
+ walk(field, section);
165
+ }
166
+ }
167
+ }
168
+ };
169
+ walk(doc, null);
170
+ return strings;
171
+ }
172
+
173
+ /**
174
+ * Where a file next holds the text, at or after a cursor (a 0-based line and
175
+ * column), else the first place it holds it, else null. A doc's strings are
176
+ * read in file order, so moving the cursor past each match puts a command the
177
+ * topic writes twice on its own line.
178
+ *
179
+ * @param {string[]} lines
180
+ * @param {string} written
181
+ * @param {{line: number, column: number}} cursor
182
+ * @returns {{line: number, column: number} | null}
183
+ */
184
+ function locate(lines, written, cursor) {
185
+ for (let at = cursor.line; at < lines.length; at++) {
186
+ const column = lines[at].indexOf(
187
+ written,
188
+ at === cursor.line ? cursor.column : 0,
189
+ );
190
+ if (column >= 0) return {line: at, column};
191
+ }
192
+ const first = lines.findIndex(line => line.includes(written));
193
+ return first < 0
194
+ ? null
195
+ : {line: first, column: lines[first].indexOf(written)};
196
+ }
197
+
198
+ /**
199
+ * An Astryx command a topic writes with a package runner in front.
200
+ *
201
+ * @typedef {object} RunnerCommand
202
+ * @property {string} topic
203
+ * @property {string | null} section the title of the section it is in
204
+ * @property {string} file the doc file
205
+ * @property {number | null} line the file line that holds it, when found
206
+ * @property {string} command the command as written
207
+ * @property {string} message the bare command to write instead
208
+ */
209
+
210
+ /**
211
+ * Runner commands in a package's topics.
212
+ *
213
+ * @param {{records: {name: string, path: string}[], guides?: {name: string, ref?: {topicFile?: string}}[]}} discovered
214
+ * the package's docs, as discoverIntegrationDocs returns them
215
+ * @returns {Promise<RunnerCommand[]>}
216
+ */
217
+ export async function findRunnerCommands(discovered) {
218
+ const topics = [
219
+ ...discovered.records.map(record => ({
220
+ name: record.name,
221
+ file: record.path,
222
+ })),
223
+ ...(discovered.guides ?? []).map(guide => ({
224
+ name: guide.name,
225
+ file: guide.ref?.topicFile,
226
+ })),
227
+ ];
228
+ /** @type {RunnerCommand[]} */
229
+ const found = [];
230
+ for (const {name, file} of topics) {
231
+ const doc = file ? await loadQuietly(file) : null;
232
+ if (!doc || !file) continue;
233
+ /** @type {string[]} */
234
+ let lines = [];
235
+ try {
236
+ lines = fs.readFileSync(file, 'utf8').split('\n');
237
+ } catch {
238
+ // The doc loaded, so only the line number is lost.
239
+ }
240
+ let cursor = {line: 0, column: 0};
241
+ for (const {text, section} of allStrings(doc)) {
242
+ const commands = runnerCommandsIn(text);
243
+ if (commands.length === 0) continue;
244
+ // A one-line string's own literal places it, wherever the walk reached
245
+ // it (a section in a constant above the doc object, say); else the
246
+ // cursor search, in file order.
247
+ const forms = [`'${text}'`, `"${text}"`, JSON.stringify(text)];
248
+ const holds = (/** @type {string} */ line) =>
249
+ forms.some(form => line.includes(form));
250
+ // On the cursor's own line, only a copy at or after the cursor counts:
251
+ // the copy just matched still sits before it.
252
+ const after = text.includes('\n')
253
+ ? -1
254
+ : lines.findIndex((line, at) =>
255
+ at > cursor.line
256
+ ? holds(line)
257
+ : at === cursor.line &&
258
+ forms.some(form => line.indexOf(form, cursor.column) >= 0),
259
+ );
260
+ const literal =
261
+ after >= 0 || text.includes('\n') ? after : lines.findIndex(holds);
262
+ const column = after >= 0 && after === cursor.line ? cursor.column : 0;
263
+ let from = literal >= 0 ? {line: literal, column} : cursor;
264
+ for (const {written, bare, download} of commands) {
265
+ const place = locate(lines, written, from);
266
+ if (place) {
267
+ from = {line: place.line, column: place.column + written.length};
268
+ if (
269
+ place.line > cursor.line ||
270
+ (place.line === cursor.line && place.column >= cursor.column)
271
+ ) {
272
+ cursor = from;
273
+ }
274
+ }
275
+ const line = place ? place.line + 1 : null;
276
+ const where = section ? ` (section "${section}")` : '';
277
+ found.push({
278
+ topic: name,
279
+ section,
280
+ file,
281
+ line,
282
+ command: written,
283
+ message:
284
+ `"${name}"${where} writes \`${written}\`${line == null ? '' : ` (line ${line})`}. ` +
285
+ `Write \`${bare}\`; the CLI shows it with your project's runner` +
286
+ (download
287
+ ? ', and a download runner with the bare name fetches an unrelated package called astryx.'
288
+ : '.'),
289
+ });
290
+ }
291
+ }
292
+ }
293
+ return found;
294
+ }