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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/README.md +104 -71
  2. package/api/build/_adapter.d.mts +58 -0
  3. package/api/build/_adapter.mjs +127 -3
  4. package/api/build/blocks/blocks.d.mts +92 -0
  5. package/api/build/blocks/blocks.mjs +528 -0
  6. package/api/build/blocks/blocks.test.mjs +238 -0
  7. package/api/build/blocks/blocks.weights.json +1 -0
  8. package/api/build/build.mjs +25 -6
  9. package/api/build/build.type.mjs +1 -1
  10. package/api/build/kit/answer.d.mts +18 -0
  11. package/api/build/kit/answer.mjs +195 -0
  12. package/api/build/kit/answer.test.mjs +108 -0
  13. package/api/build/kit/kind.d.mts +25 -0
  14. package/api/build/kit/kind.mjs +170 -0
  15. package/api/build/kit/kit.d.mts +2 -4
  16. package/api/build/kit/kit.mjs +8 -398
  17. package/api/build/kit/rank.d.mts +3 -32
  18. package/api/build/kit/rank.mjs +25 -342
  19. package/api/build/kit/rank.test.mjs +2 -1
  20. package/api/build/kit/signals.d.mts +79 -0
  21. package/api/build/kit/signals.mjs +295 -0
  22. package/api/build/kit/start.d.mts +32 -0
  23. package/api/build/kit/start.mjs +163 -0
  24. package/api/build/kit/words.d.mts +72 -0
  25. package/api/build/kit/words.mjs +202 -0
  26. package/api/build/tree.d.mts +20 -0
  27. package/api/build/tree.mjs +53 -0
  28. package/api/build/tree.test.mjs +185 -0
  29. package/api/build/tree.weights.json +1 -0
  30. package/api/component/_adapter.mjs +39 -5
  31. package/api/component/component-replacement.test.mjs +1 -0
  32. package/api/component/component.mjs +17 -3
  33. package/api/component/component.test.mjs +15 -0
  34. package/api/component/doc-links.test.mjs +2 -2
  35. package/api/discover/_adapter.d.mts +1 -2
  36. package/api/discover/_adapter.mjs +42 -17
  37. package/api/discover/_adapter.test.mjs +42 -0
  38. package/api/discover/discover.mjs +0 -1
  39. package/api/docs/_adapter.d.mts +2 -2
  40. package/api/docs/_adapter.mjs +82 -23
  41. package/api/docs/docs-homes.test.mjs +127 -0
  42. package/api/docs/docs.doc.mjs +3 -3
  43. package/api/docs/docs.test.mjs +35 -19
  44. package/api/docs/integration-tree.test.mjs +20 -20
  45. package/api/docs/integrationDocs.test.mjs +1 -1
  46. package/api/docs/namespace-section.test.mjs +6 -6
  47. package/api/docs/reference-blocks.test.mjs +2 -2
  48. package/api/doctor/_adapter.d.mts +33 -13
  49. package/api/doctor/_adapter.mjs +55 -25
  50. package/api/doctor/doctor.d.mts +23 -5
  51. package/api/doctor/doctor.mjs +183 -40
  52. package/api/doctor/doctor.test.mjs +95 -12
  53. package/api/hook/hook.mjs +1 -1
  54. package/api/init/init.doc.mjs +20 -6
  55. package/api/init/init.test.mjs +18 -2
  56. package/api/init/init.type.d.mts +3 -3
  57. package/api/init/init.type.mjs +3 -3
  58. package/api/init/remove/remove.mjs +17 -4
  59. package/api/init/remove/remove.test.mjs +114 -2
  60. package/api/init/run/run.mjs +69 -6
  61. package/api/integration/_adapter.d.mts +8 -0
  62. package/api/integration/_adapter.mjs +21 -1
  63. package/api/integration/authoring-checks.mjs +83 -9
  64. package/api/integration/authoring-checks.test.mjs +220 -0
  65. package/api/integration/pack-check.mjs +123 -4
  66. package/api/integration/pack-check.test.mjs +87 -0
  67. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  68. package/api/integration/validate-integration-fixes.test.mjs +6 -5
  69. package/api/integration/validate-integration-gaps.test.mjs +1975 -0
  70. package/api/integration/validate-integration.d.mts +15 -2
  71. package/api/integration/validate-integration.mjs +298 -39
  72. package/api/integration/validate-integration.type.d.mts +90 -1
  73. package/api/integration/validate-integration.type.mjs +67 -2
  74. package/api/integration/validateIntegration.doc.mjs +1 -1
  75. package/api/search/ORDERING.md +28 -0
  76. package/api/search/_adapter.d.mts +82 -2
  77. package/api/search/_adapter.mjs +157 -10
  78. package/api/search/_adapter.test.mjs +173 -0
  79. package/api/search/_candidate-score.d.mts +40 -0
  80. package/api/search/_candidate-score.mjs +238 -0
  81. package/api/search/_cards.json +6098 -0
  82. package/api/search/_compound.d.mts +50 -0
  83. package/api/search/_compound.mjs +170 -0
  84. package/api/search/_doc-searcher.d.mts +4 -0
  85. package/api/search/_doc-searcher.mjs +23 -0
  86. package/api/search/_doc-searcher.test.mjs +36 -0
  87. package/api/search/_doc-searcher.weights.json +1 -0
  88. package/api/search/_doc-text.d.mts +30 -0
  89. package/api/search/_doc-text.mjs +87 -0
  90. package/api/search/_entries.d.mts +63 -0
  91. package/api/search/_entries.mjs +232 -0
  92. package/api/search/_entries.test.mjs +48 -0
  93. package/api/search/_features.d.mts +35 -0
  94. package/api/search/_features.mjs +155 -0
  95. package/api/search/_gather-components.d.mts +26 -0
  96. package/api/search/_gather-components.mjs +284 -0
  97. package/api/search/_gather-docs.d.mts +20 -0
  98. package/api/search/_gather-docs.mjs +254 -0
  99. package/api/search/_gather-hooks.d.mts +12 -0
  100. package/api/search/_gather-hooks.mjs +52 -0
  101. package/api/search/_gather-templates.d.mts +25 -0
  102. package/api/search/_gather-templates.mjs +107 -0
  103. package/api/search/_lexical.d.mts +135 -0
  104. package/api/search/_lexical.mjs +351 -0
  105. package/api/search/_main-searcher.d.mts +19 -0
  106. package/api/search/_main-searcher.mjs +115 -0
  107. package/api/search/_main-searcher.test.mjs +106 -0
  108. package/api/search/_main-searcher.weights.json +1 -0
  109. package/api/search/_module-doc.d.mts +32 -0
  110. package/api/search/_module-doc.mjs +78 -0
  111. package/api/search/_order.d.mts +32 -0
  112. package/api/search/_order.mjs +162 -0
  113. package/api/search/_order.test.mjs +72 -0
  114. package/api/search/_priority.d.mts +50 -0
  115. package/api/search/_priority.mjs +139 -0
  116. package/api/search/_query-words.d.mts +25 -0
  117. package/api/search/_query-words.mjs +92 -0
  118. package/api/search/_query.d.mts +65 -0
  119. package/api/search/_query.mjs +176 -0
  120. package/api/search/_result.d.mts +97 -0
  121. package/api/search/_result.mjs +150 -0
  122. package/api/search/_score.d.mts +60 -0
  123. package/api/search/_score.mjs +283 -0
  124. package/api/search/_synonyms.d.mts +21 -0
  125. package/api/search/_synonyms.mjs +67 -0
  126. package/api/search/_template-searcher.d.mts +4 -0
  127. package/api/search/_template-searcher.mjs +23 -0
  128. package/api/search/_template-searcher.test.mjs +40 -0
  129. package/api/search/_template-searcher.weights.json +1 -0
  130. package/api/search/_titles.d.mts +33 -0
  131. package/api/search/_titles.mjs +65 -0
  132. package/api/search/_words.d.mts +64 -0
  133. package/api/search/_words.mjs +157 -0
  134. package/api/search/search.d.mts +22 -305
  135. package/api/search/search.doc.mjs +8 -6
  136. package/api/search/search.mjs +56 -2482
  137. package/api/search/search.test.mjs +14 -14
  138. package/api/search/search.type.d.mts +6 -1
  139. package/api/search/search.type.mjs +6 -1
  140. package/api/search/tree.d.mts +15 -0
  141. package/api/search/tree.mjs +40 -0
  142. package/api/search/tree.test.mjs +66 -0
  143. package/api/search/tree.weights.json +1 -0
  144. package/api/template/template.mjs +5 -1
  145. package/api/theme/build/build.packed-old-core.test.mjs +1 -0
  146. package/api/upgrade/_adapter.d.mts +57 -9
  147. package/api/upgrade/_adapter.mjs +198 -28
  148. package/api/upgrade/registry/registry.d.mts +10 -4
  149. package/api/upgrade/registry/registry.mjs +33 -16
  150. package/api/upgrade/registry/registry.test.mjs +85 -0
  151. package/api/upgrade/run/run.d.mts +4 -2
  152. package/api/upgrade/run/run.mjs +13 -2
  153. package/api/upgrade/run/run.test.mjs +417 -3
  154. package/api/upgrade/upgrade.doc.mjs +10 -5
  155. package/api/upgrade/upgrade.mjs +13 -2
  156. package/api/upgrade/upgrade.test.mjs +194 -0
  157. package/api/upgrade/upgrade.type.d.mts +5 -3
  158. package/api/upgrade/upgrade.type.mjs +8 -6
  159. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  160. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  161. package/assets/docs/author-a-theme.doc.mjs +2 -2
  162. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  163. package/assets/docs/tree/add-a-component.doc.mjs +1 -1
  164. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  165. package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
  166. package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
  167. package/assets/docs/tree/api.doc.mjs +6 -0
  168. package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
  169. package/assets/docs/tree/block-template.doc.mjs +1 -1
  170. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  171. package/assets/docs/tree/building-blocks.doc.mjs +2 -2
  172. package/assets/docs/tree/check-the-props.doc.mjs +1 -1
  173. package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
  174. package/assets/docs/tree/checks.doc.mjs +3 -3
  175. package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
  176. package/assets/docs/tree/cli-command.doc.mjs +1 -1
  177. package/assets/docs/tree/cli.doc.mjs +1 -1
  178. package/assets/docs/tree/codemods.doc.mjs +1 -1
  179. package/assets/docs/tree/component-family.doc.mjs +1 -1
  180. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  181. package/assets/docs/tree/components.doc.mjs +1 -1
  182. package/assets/docs/tree/configuration.doc.mjs +1 -1
  183. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
  184. package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
  185. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  186. package/assets/docs/tree/docs.doc.mjs +1 -1
  187. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  188. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  189. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  190. package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
  191. package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
  192. package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
  193. package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
  194. package/assets/docs/tree/global-integrations.doc.mjs +2 -2
  195. package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
  196. package/assets/docs/tree/help.doc.mjs +2 -2
  197. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  198. package/assets/docs/tree/links.doc.mjs +1 -1
  199. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  200. package/assets/docs/tree/page-template.doc.mjs +1 -1
  201. package/assets/docs/tree/publishing.doc.mjs +1 -1
  202. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  203. package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
  204. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
  207. package/assets/docs/tree/ship.doc.mjs +2 -2
  208. package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
  209. package/assets/docs/tree/single-component.doc.mjs +1 -1
  210. package/assets/docs/tree/start-a-template.doc.mjs +1 -1
  211. package/assets/docs/tree/subcomponent.doc.mjs +1 -1
  212. package/assets/docs/tree/template-assets.doc.mjs +1 -1
  213. package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
  214. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  215. package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
  216. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  217. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  218. package/assets/docs/tree/template-styles.doc.mjs +1 -1
  219. package/assets/docs/tree/templates.doc.mjs +1 -1
  220. package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
  221. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  222. package/assets/docs/tree/themes.doc.mjs +1 -1
  223. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  224. package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
  225. package/assets/docs/tree/upgrading.doc.mjs +1 -1
  226. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
  227. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  228. package/assets/docs/tree/versioning.doc.mjs +5 -1
  229. package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
  230. package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
  231. package/assets/docs/use-integrations.doc.mjs +120 -0
  232. package/assets/docs/working-with-ai.doc.mjs +6 -2
  233. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  234. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  235. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  236. package/authoring/debug/parse.d.mts +1 -1
  237. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  238. package/authoring/integration/integration.doc.mjs +1 -1
  239. package/authoring/integration/type.ts +1 -1
  240. package/clients/cli/bin/astryx.mjs +7 -78
  241. package/clients/cli/command-result-coverage.test.mjs +6 -5
  242. package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
  243. package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
  244. package/clients/cli/commands/component.test.mjs +6 -6
  245. package/clients/cli/commands/docs.doc.mjs +2 -2
  246. package/clients/cli/commands/docs.mjs +1 -1
  247. package/clients/cli/commands/docs.test.mjs +14 -14
  248. package/clients/cli/commands/doctor-integration.test.mjs +49 -1
  249. package/clients/cli/commands/doctor.doc.mjs +1 -1
  250. package/clients/cli/commands/doctor.mjs +40 -0
  251. package/clients/cli/commands/doctor.test.mjs +65 -0
  252. package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
  253. package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
  254. package/clients/cli/commands/init.behavior.test.mjs +230 -1
  255. package/clients/cli/commands/init.doc.mjs +28 -9
  256. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  257. package/clients/cli/commands/init.mjs +2 -2
  258. package/clients/cli/commands/integration-check.test.mjs +48 -0
  259. package/clients/cli/commands/integration.doc.mjs +1 -1
  260. package/clients/cli/commands/manifest.doc.mjs +1 -1
  261. package/clients/cli/commands/manifest.mjs +67 -0
  262. package/clients/cli/commands/outside-project.test.mjs +101 -1
  263. package/clients/cli/commands/postinstall.doc.mjs +23 -0
  264. package/clients/cli/commands/postinstall.mjs +31 -0
  265. package/clients/cli/commands/setup-nudge.test.mjs +78 -0
  266. package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
  267. package/clients/cli/commands/upgrade.doc.mjs +4 -2
  268. package/clients/cli/commands/upgrade.mjs +9 -2
  269. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  270. package/clients/cli/index.mjs +54 -94
  271. package/clients/cli/lib/component-format.mjs +1 -1
  272. package/clients/cli/lib/define-command.mjs +12 -7
  273. package/clients/cli/lib/exit-codes.test.mjs +5 -0
  274. package/clients/cli/lib/manifest.test.mjs +12 -0
  275. package/clients/cli/lib/welcome-box.mjs +50 -0
  276. package/clients/cli/run.mjs +108 -0
  277. package/foundation/agent-docs/agent-doc-state.d.mts +32 -7
  278. package/foundation/agent-docs/agent-doc-state.mjs +58 -9
  279. package/foundation/agent-docs/agent-docs.d.mts +100 -12
  280. package/foundation/agent-docs/agent-docs.mjs +1017 -105
  281. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  282. package/foundation/agent-docs/agent-docs.test.mjs +683 -39
  283. package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
  284. package/foundation/agent-docs/computer-agent-text.mjs +135 -8
  285. package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
  286. package/foundation/config/project.mjs +20 -22
  287. package/foundation/config/project.test.mjs +56 -0
  288. package/foundation/discovery/authoring-self-docs.d.mts +7 -0
  289. package/foundation/discovery/authoring-self-docs.mjs +16 -4
  290. package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
  291. package/foundation/discovery/authoring-surface.d.mts +1 -1
  292. package/foundation/discovery/authoring-surface.mjs +2 -2
  293. package/foundation/discovery/authoring-surface.test.mjs +1 -1
  294. package/foundation/discovery/cli-self-docs.d.mts +2 -1
  295. package/foundation/discovery/cli-self-docs.mjs +4 -3
  296. package/foundation/discovery/cli-self-docs.test.mjs +2 -2
  297. package/foundation/discovery/component-discovery.d.mts +37 -0
  298. package/foundation/discovery/component-discovery.mjs +80 -5
  299. package/foundation/discovery/component-loader.d.mts +1 -1
  300. package/foundation/discovery/docs-discovery.mjs +14 -1
  301. package/foundation/discovery/module-exports.d.mts +41 -0
  302. package/foundation/discovery/module-exports.mjs +253 -0
  303. package/foundation/discovery/runner-allowlist.d.mts +2 -4
  304. package/foundation/discovery/runner-allowlist.mjs +2 -4
  305. package/foundation/discovery/runner-commands.d.mts +77 -0
  306. package/foundation/discovery/runner-commands.mjs +294 -0
  307. package/foundation/discovery/runner-commands.test.mjs +128 -0
  308. package/foundation/discovery/stray-pointers.test.mjs +1 -1
  309. package/foundation/discovery/template-adapter.mjs +18 -1
  310. package/foundation/discovery/template-needs.mjs +1 -44
  311. package/foundation/discovery/theme-discovery.mjs +147 -3
  312. package/foundation/doc-compiler/doc-loads.test.mjs +33 -9
  313. package/foundation/doc-compiler/read.mjs +59 -1
  314. package/foundation/doc-compiler/read.test.mjs +11 -2
  315. package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
  316. package/foundation/doc-compiler/tree.d.mts +32 -6
  317. package/foundation/doc-compiler/tree.mjs +77 -43
  318. package/foundation/doc-compiler/tree.test.mjs +7 -7
  319. package/foundation/fs/paths.d.mts +70 -8
  320. package/foundation/fs/paths.mjs +556 -38
  321. package/foundation/fs/paths.test.mjs +430 -54
  322. package/foundation/fs/read-record.d.mts +18 -0
  323. package/foundation/fs/read-record.mjs +131 -0
  324. package/foundation/fs/read-record.test.mjs +77 -0
  325. package/foundation/integrations/autolink.d.mts +2 -1
  326. package/foundation/integrations/autolink.mjs +4 -13
  327. package/foundation/integrations/cli-peer-version.d.mts +36 -0
  328. package/foundation/integrations/cli-peer-version.mjs +132 -0
  329. package/foundation/integrations/cli-peer-version.test.mjs +354 -0
  330. package/foundation/integrations/computer-integrations.d.mts +6 -3
  331. package/foundation/integrations/computer-integrations.mjs +38 -9
  332. package/foundation/integrations/contribution-inventory.d.mts +15 -20
  333. package/foundation/integrations/contribution-inventory.mjs +41 -5
  334. package/foundation/integrations/exports-subpaths.d.mts +57 -0
  335. package/foundation/integrations/exports-subpaths.mjs +113 -0
  336. package/foundation/integrations/integrations.d.mts +27 -1
  337. package/foundation/integrations/integrations.mjs +61 -2
  338. package/foundation/integrations/public-imports.d.mts +118 -0
  339. package/foundation/integrations/public-imports.mjs +1769 -0
  340. package/foundation/integrations/public-imports.test.mjs +1793 -0
  341. package/foundation/integrations/validate-contributions.d.mts +115 -0
  342. package/foundation/integrations/validate-contributions.mjs +1168 -13
  343. package/foundation/release/cache.test.mjs +24 -21
  344. package/foundation/release/global-state.d.mts +8 -4
  345. package/foundation/release/global-state.mjs +4 -2
  346. package/foundation/release/handoff.d.mts +23 -4
  347. package/foundation/release/handoff.mjs +175 -8
  348. package/foundation/release/notices.d.mts +62 -8
  349. package/foundation/release/notices.mjs +136 -25
  350. package/foundation/release/notices.test.mjs +21 -0
  351. package/foundation/release/run-context.mjs +14 -0
  352. package/foundation/response/response-types.doc.mjs +1 -1
  353. package/foundation/route/route.d.mts +285 -0
  354. package/foundation/route/route.mjs +324 -0
  355. package/foundation/route/route.test.mjs +153 -0
  356. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  357. package/foundation/runtime/locked-versions.d.mts +32 -0
  358. package/foundation/runtime/locked-versions.mjs +65 -0
  359. package/foundation/runtime/package-root.d.mts +91 -0
  360. package/foundation/runtime/package-root.mjs +174 -0
  361. package/foundation/runtime/package-set.d.mts +62 -0
  362. package/foundation/runtime/package-set.mjs +130 -0
  363. package/foundation/runtime/package-set.test.mjs +432 -0
  364. package/foundation/text/string-utils.d.mts +2 -1
  365. package/foundation/text/string-utils.mjs +16 -5
  366. package/foundation/text/suggest-order.d.mts +72 -0
  367. package/foundation/text/suggest-order.mjs +363 -0
  368. package/foundation/text/suggest-order.test.mjs +259 -0
  369. package/host/door-conformance.test.mjs +274 -0
  370. package/host/index.d.ts +116 -0
  371. package/host/index.mjs +295 -0
  372. package/host/index.test.mjs +302 -0
  373. package/package.json +14 -9
@@ -4,7 +4,7 @@ import {describe, it, expect, beforeEach, afterEach} from 'vitest';
4
4
  import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import * as os from 'node:os';
7
- import {findCoreDir, findProjectCoreDir, findProjectRoot, listComponents, discoverExternalPackages, existsCaseExact, findInstalledPackage} from './paths.mjs';
7
+ import {findAstryxProject, findCoreDir, findProjectCoreDir, findProjectRoot, listComponents, discoverExternalPackages, existsCaseExact, findInstalledPackage, findNodeModulesPackage, findPackageRoot, findProjectPackageOf, findWorkspaceRoot} from './paths.mjs';
8
8
  import {CLI_ROOT} from './paths.mjs';
9
9
  import {linkThisCliAsProjectCli} from '../../test-utils/release-fixtures.mjs';
10
10
 
@@ -19,13 +19,93 @@ beforeEach(() => {
19
19
  });
20
20
 
21
21
  afterEach(() => {
22
+ delete process.versions.pnp;
22
23
  fs.rmSync(tmpDir, {recursive: true, force: true});
23
24
  });
24
25
 
26
+ /**
27
+ * Write a package.json naming `name` into `dir`, creating the folder.
28
+ * @param {string} dir @param {string} name @param {string} [version]
29
+ */
30
+ function writePackage(dir, name, version = '0.7.0') {
31
+ fs.mkdirSync(dir, {recursive: true});
32
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({name, version}));
33
+ return dir;
34
+ }
35
+
36
+ /**
37
+ * Write a project package.json into `dir`, creating the folder.
38
+ * @param {string} dir @param {Record<string, unknown>} [fields]
39
+ */
40
+ function writeProject(dir, fields = {}) {
41
+ fs.mkdirSync(dir, {recursive: true});
42
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({name: path.basename(dir), ...fields}));
43
+ return dir;
44
+ }
45
+
46
+ /**
47
+ * Run `fn` with `home` as the home folder.
48
+ * @template T
49
+ * @param {string} home @param {() => T} fn
50
+ * @returns {T}
51
+ */
52
+ function withHome(home, fn) {
53
+ const previous = process.env.HOME;
54
+ process.env.HOME = home;
55
+ try {
56
+ return fn();
57
+ } finally {
58
+ if (previous === undefined) delete process.env.HOME;
59
+ else process.env.HOME = previous;
60
+ }
61
+ }
62
+
63
+ /** Declares Core, which makes a package.json an Astryx project. */
64
+ const DECLARES_CORE = {dependencies: {'@astryxdesign/core': '^0.7.0'}};
65
+
66
+ /** @param {string} root @param {string} name @param {string} version */
67
+ function install(root, name, version) {
68
+ return writePackage(path.join(root, 'node_modules', ...name.split('/')), name, version);
69
+ }
70
+
71
+ /**
72
+ * Stand in for the runtime a Yarn Plug'n'Play process exposes:
73
+ * `process.versions.pnp` plus a requirable `pnpapi`. The packages themselves
74
+ * live outside any node_modules, the way Yarn stores them. The top-level
75
+ * afterEach removes `process.versions.pnp` again.
76
+ * @param {Record<string, string>} resolved name -> package directory
77
+ * @returns {string} a project folder inside the PnP runtime
78
+ */
79
+ function withPnp(resolved) {
80
+ const runtime = path.join(tmpDir, 'runtime');
81
+ const api = path.join(runtime, 'node_modules', 'pnpapi');
82
+ fs.mkdirSync(api, {recursive: true});
83
+ fs.writeFileSync(
84
+ path.join(api, 'package.json'),
85
+ JSON.stringify({name: 'pnpapi', version: '1.0.0', main: 'index.js'}),
86
+ );
87
+ fs.writeFileSync(
88
+ path.join(api, 'index.js'),
89
+ `const resolved = ${JSON.stringify(resolved)};\n` +
90
+ 'module.exports = {\n' +
91
+ ' resolveToUnqualified(name) {\n' +
92
+ ' if (!(name in resolved)) {\n' +
93
+ " const e = new Error(name); e.code = 'MODULE_NOT_FOUND'; throw e;\n" +
94
+ ' }\n' +
95
+ ' return resolved[name];\n' +
96
+ ' },\n' +
97
+ '};\n',
98
+ );
99
+ Object.defineProperty(process.versions, 'pnp', {
100
+ value: '3',
101
+ configurable: true,
102
+ });
103
+ return runtime;
104
+ }
105
+
25
106
  describe('findCoreDir', () => {
26
107
  it('finds packages/core walking up directories', () => {
27
- const coreDir = path.join(tmpDir, 'packages', 'core');
28
- fs.mkdirSync(coreDir, {recursive: true});
108
+ const coreDir = writePackage(path.join(tmpDir, 'packages', 'core'), '@astryxdesign/core');
29
109
 
30
110
  const nested = path.join(tmpDir, 'sub', 'deep');
31
111
  fs.mkdirSync(nested, {recursive: true});
@@ -34,8 +114,7 @@ describe('findCoreDir', () => {
34
114
  });
35
115
 
36
116
  it('finds node_modules/@astryxdesign/core fallback', () => {
37
- const nmCore = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
38
- fs.mkdirSync(nmCore, {recursive: true});
117
+ const nmCore = install(tmpDir, '@astryxdesign/core', '0.7.0');
39
118
 
40
119
  expect(findCoreDir(tmpDir)).toBe(nmCore);
41
120
  });
@@ -48,22 +127,111 @@ describe('findCoreDir', () => {
48
127
  // This copy's carried Core is the workspace Core beside it.
49
128
  expect(findCoreDir(tmpDir)).toBe(path.resolve(CLI_ROOT, '..', 'core'));
50
129
  linkThisCliAsProjectCli(tmpDir);
51
- expect(findCoreDir(tmpDir)).toBeNull();
130
+ // The project's own Core: findCoreDir falls back to the carried one.
131
+ expect(findProjectCoreDir(tmpDir)).toBeNull();
132
+ });
133
+
134
+ // A route folder such as src/app/(shop)/orders/[id]/edit sits six folders
135
+ // below the project root, where Core is installed.
136
+ it('finds Core from any depth inside the project that installs it', () => {
137
+ writeProject(tmpDir, {dependencies: {'@astryxdesign/core': '^0.7.0'}});
138
+ const nmCore = install(tmpDir, '@astryxdesign/core', '0.7.0');
139
+ const route = path.join(tmpDir, 'src', 'app', '(shop)', 'orders', '[id]', 'edit');
140
+ fs.mkdirSync(route, {recursive: true});
141
+
142
+ expect(findCoreDir(route)).toBe(nmCore);
143
+ });
144
+
145
+ it('searches a workspace up to its root from any depth in a member', () => {
146
+ writeProject(tmpDir, {private: true, workspaces: ['apps/*']});
147
+ const nmCore = install(tmpDir, '@astryxdesign/core', '0.7.0');
148
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'), {dependencies: {'@astryxdesign/core': '^0.7.0'}});
149
+ const route = path.join(member, 'src', 'app', '(shop)', 'orders', '[id]');
150
+ fs.mkdirSync(route, {recursive: true});
151
+
152
+ expect(findCoreDir(route)).toBe(nmCore);
153
+ });
154
+
155
+ it('searches only the folder and the four above it outside any Astryx project', () => {
156
+ install(tmpDir, '@astryxdesign/core', '0.7.0');
157
+ const project = writeProject(path.join(tmpDir, 'a', 'b', 'c', 'd', 'e', 'app'));
158
+
159
+ // The project's own Core: findCoreDir falls back to the carried one.
160
+ expect(findProjectCoreDir(project)).toBeNull();
161
+ });
162
+
163
+ // A package.json that only sets the module type is not the project, so a
164
+ // folder deep below it still reaches the Core the project installs.
165
+ it('looks past a nested package.json that declares no Astryx package', () => {
166
+ writeProject(tmpDir, DECLARES_CORE);
167
+ const nmCore = install(tmpDir, '@astryxdesign/core', '0.7.0');
168
+ fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
169
+ fs.writeFileSync(path.join(tmpDir, 'src', 'package.json'), JSON.stringify({type: 'module'}));
170
+ const deep = path.join(tmpDir, 'src', 'l1', 'l2', 'l3', 'l4', 'l5', 'l6');
171
+ fs.mkdirSync(deep, {recursive: true});
172
+
173
+ expect(findCoreDir(deep)).toBe(nmCore);
174
+ });
175
+
176
+ // Past the reach the search starts at the project root: a tool folder
177
+ // between them that installs its own Core but declares nothing never
178
+ // answers for the project, as `upgrade` targets the project's Core.
179
+ it('answers the project\'s Core, not a tool folder\'s, from deep inside the tool', () => {
180
+ const app = path.join(tmpDir, 'app');
181
+ writeProject(app, DECLARES_CORE);
182
+ const projectCore = install(app, '@astryxdesign/core', '0.7.0');
183
+ const tool = path.join(app, 'tools', 'codegen');
184
+ writePackage(tool, 'codegen');
185
+ install(tool, '@astryxdesign/core', '0.9.0');
186
+ const deep = path.join(tool, 'src', 'a', 'b', 'c', 'd', 'e');
187
+ fs.mkdirSync(deep, {recursive: true});
188
+
189
+ expect(findProjectCoreDir(deep)).toBe(projectCore);
190
+ expect(findInstalledPackage(path.join(deep, 'f'), '@astryxdesign/core')).toBe(projectCore);
191
+ });
192
+
193
+ // Node loads the hoisted copy whatever the member declares: an override or
194
+ // resolution can force it, and a peer range can be stale. Reads answer from
195
+ // the copy that loads.
196
+ it.each([
197
+ ['a range the copy is outside of', {devDependencies: {'@astryxdesign/core': '~0.6.8'}}],
198
+ ['only a stale peer range', {peerDependencies: {'@astryxdesign/core': '^0.6.8'}}],
199
+ ])('takes a workspace Core when the member declares %s', (_label, fields) => {
200
+ writeProject(tmpDir, {private: true, workspaces: ['apps/*'], overrides: {'@astryxdesign/core': '0.7.1'}});
201
+ const nmCore = install(tmpDir, '@astryxdesign/core', '0.7.1');
202
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'), fields);
203
+
204
+ expect(findCoreDir(member)).toBe(nmCore);
205
+ });
206
+
207
+ it('skips a packages/core folder that holds another package', () => {
208
+ writePackage(path.join(tmpDir, 'packages', 'core'), '@acme/core', '1.0.0');
209
+ const app = path.join(tmpDir, 'apps', 'web');
210
+ fs.mkdirSync(app, {recursive: true});
211
+
212
+ // The project's own Core: findCoreDir falls back to the carried one.
213
+ expect(findProjectCoreDir(app)).toBeNull();
214
+
215
+ const nmCore = install(tmpDir, '@astryxdesign/core', '0.7.0');
216
+ expect(findCoreDir(app)).toBe(nmCore);
217
+ });
218
+
219
+ it('skips a node_modules/@astryxdesign/core folder with no package.json', () => {
220
+ fs.mkdirSync(path.join(tmpDir, 'node_modules', '@astryxdesign', 'core'), {recursive: true});
221
+
222
+ // The project's own Core: findCoreDir falls back to the carried one.
223
+ expect(findProjectCoreDir(tmpDir)).toBeNull();
224
+ });
225
+
226
+ it("asks Yarn Plug'n'Play when no folder holds Core", () => {
227
+ const store = writePackage(path.join(tmpDir, 'store', '@astryxdesign-core'), '@astryxdesign/core');
228
+ const project = withPnp({'@astryxdesign/core': store});
229
+
230
+ expect(findCoreDir(project)).toBe(store);
52
231
  });
53
232
  });
54
233
 
55
234
  describe('findInstalledPackage', () => {
56
- /** @param {string} root @param {string} name @param {string} version */
57
- function install(root, name, version) {
58
- const dir = path.join(root, 'node_modules', ...name.split('/'));
59
- fs.mkdirSync(dir, {recursive: true});
60
- fs.writeFileSync(
61
- path.join(dir, 'package.json'),
62
- JSON.stringify({name, version}),
63
- );
64
- return dir;
65
- }
66
-
67
235
  it('finds a package in the node_modules chain above startDir', () => {
68
236
  const installed = install(tmpDir, '@astryxdesign/core', '0.6.0');
69
237
  const nested = path.join(tmpDir, 'src', 'deep');
@@ -72,6 +240,22 @@ describe('findInstalledPackage', () => {
72
240
  expect(findInstalledPackage(nested, '@astryxdesign/core')).toBe(installed);
73
241
  });
74
242
 
243
+ it('finds a package from any depth inside the project', () => {
244
+ writeProject(tmpDir, DECLARES_CORE);
245
+ const installed = install(tmpDir, '@astryxdesign/core', '0.7.0');
246
+ const deep = path.join(tmpDir, 'apps', 'web', 'src', 'app', '(shop)', 'orders', '[id]');
247
+ fs.mkdirSync(deep, {recursive: true});
248
+
249
+ expect(findInstalledPackage(deep, '@astryxdesign/core')).toBe(installed);
250
+ });
251
+
252
+ it('does not search past six folders outside the project', () => {
253
+ install(tmpDir, '@astryxdesign/core', '0.7.0');
254
+ const project = writeProject(path.join(tmpDir, 'a', 'b', 'c', 'd', 'e', 'f', 'app'));
255
+
256
+ expect(findInstalledPackage(project, '@astryxdesign/core')).toBeNull();
257
+ });
258
+
75
259
  it('reports a package the project never installed as missing', () => {
76
260
  expect(findInstalledPackage(tmpDir, '@astryxdesign/core')).toBeNull();
77
261
  });
@@ -96,43 +280,6 @@ describe('findInstalledPackage', () => {
96
280
  });
97
281
 
98
282
  describe('under Yarn Plug\'n\'Play, which has no node_modules', () => {
99
- /**
100
- * Stand in for the runtime a PnP process exposes: `process.versions.pnp`
101
- * plus a requirable `pnpapi`. The package itself lives outside any
102
- * node_modules, the way Yarn stores it.
103
- * @param {Record<string, string>} resolved name -> package directory
104
- */
105
- function withPnp(resolved) {
106
- const runtime = path.join(tmpDir, 'runtime');
107
- const api = path.join(runtime, 'node_modules', 'pnpapi');
108
- fs.mkdirSync(api, {recursive: true});
109
- fs.writeFileSync(
110
- path.join(api, 'package.json'),
111
- JSON.stringify({name: 'pnpapi', version: '1.0.0', main: 'index.js'}),
112
- );
113
- fs.writeFileSync(
114
- path.join(api, 'index.js'),
115
- `const resolved = ${JSON.stringify(resolved)};\n` +
116
- 'module.exports = {\n' +
117
- ' resolveToUnqualified(name) {\n' +
118
- ' if (!(name in resolved)) {\n' +
119
- " const e = new Error(name); e.code = 'MODULE_NOT_FOUND'; throw e;\n" +
120
- ' }\n' +
121
- ' return resolved[name];\n' +
122
- ' },\n' +
123
- '};\n',
124
- );
125
- Object.defineProperty(process.versions, 'pnp', {
126
- value: '3',
127
- configurable: true,
128
- });
129
- return runtime;
130
- }
131
-
132
- afterEach(() => {
133
- delete process.versions.pnp;
134
- });
135
-
136
283
  it('falls back to the PnP runtime when there is no chain to walk', () => {
137
284
  const store = path.join(tmpDir, 'store', '@astryxdesign-core');
138
285
  fs.mkdirSync(store, {recursive: true});
@@ -153,6 +300,235 @@ describe('findInstalledPackage', () => {
153
300
  });
154
301
  });
155
302
 
303
+ describe('findProjectPackageOf', () => {
304
+ const NAMES = ['@astryxdesign/core', '@xds/core'];
305
+
306
+ it('takes the earlier name when one folder installs both', () => {
307
+ writeProject(tmpDir, DECLARES_CORE);
308
+ const current = install(tmpDir, '@astryxdesign/core', '0.7.0');
309
+ install(tmpDir, '@xds/core', '0.3.0');
310
+
311
+ expect(findProjectPackageOf(tmpDir, NAMES)).toEqual({name: '@astryxdesign/core', packageDir: current});
312
+ });
313
+
314
+ it('takes the nearest folder over an earlier name further up', () => {
315
+ writeProject(tmpDir, {private: true, workspaces: ['apps/*']});
316
+ install(tmpDir, '@astryxdesign/core', '0.7.0');
317
+ const member = writeProject(path.join(tmpDir, 'apps', 'legacy'), {dependencies: {'@xds/core': '0.3.0'}});
318
+ const legacy = install(member, '@xds/core', '0.3.0');
319
+
320
+ expect(findProjectPackageOf(member, NAMES)).toEqual({name: '@xds/core', packageDir: legacy});
321
+ });
322
+
323
+ it('returns null when none is installed', () => {
324
+ expect(findProjectPackageOf(tmpDir, NAMES)).toBeNull();
325
+ });
326
+
327
+ it('finds the package from a subfolder of the project', () => {
328
+ writeProject(tmpDir, DECLARES_CORE);
329
+ const current = install(tmpDir, '@astryxdesign/core', '0.7.0');
330
+ const deep = path.join(tmpDir, 'src', 'l1', 'l2');
331
+ fs.mkdirSync(deep, {recursive: true});
332
+
333
+ expect(findProjectPackageOf(deep, NAMES)).toEqual({name: '@astryxdesign/core', packageDir: current});
334
+ });
335
+
336
+ // A member uses what its workspace installs, whatever its own package.json
337
+ // says: an override or resolution can force the copy, and a range or peer
338
+ // range can be stale. Membership decides, not the declared version.
339
+ it.each([
340
+ ['~0.6.8', 'dependencies'],
341
+ ['0.7.0 - 0.8.0', 'dependencies'],
342
+ ['>= 0.7.0', 'dependencies'],
343
+ ['^0.6.8', 'peerDependencies'],
344
+ ])('takes the hoisted package for a member that declares %s in %s', (range, field) => {
345
+ writeProject(tmpDir, {private: true, workspaces: ['apps/*']});
346
+ const hoisted = install(tmpDir, '@astryxdesign/core', '0.7.1');
347
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'), {[field]: {'@astryxdesign/core': range}});
348
+
349
+ expect(findProjectPackageOf(member, NAMES)).toEqual({name: '@astryxdesign/core', packageDir: hoisted});
350
+ });
351
+
352
+ it.each([
353
+ ['Yarn object-form workspaces', {workspaces: {packages: ['apps/*']}}, null],
354
+ ['a ** pattern', {workspaces: ['apps/**']}, null],
355
+ ['a brace pattern', {workspaces: ['{apps,tools}/*']}, null],
356
+ ['a brace list of globs', {workspaces: ['{apps/*,tools/*}']}, null],
357
+ ['a nested brace list', {workspaces: ['{apps/{web,admin},tools/*}']}, null],
358
+ ['a pnpm-workspace.yaml block list', {}, "packages:\n - 'apps/*' # apps\n - \"tools/*\"\nonlyBuiltDependencies: []\n"],
359
+ ['a pnpm-workspace.yaml flow list', {}, "packages: ['tools/*', apps/*]\n"],
360
+ ])('takes the hoisted package for a member that %s lists', (_label, rootFields, yaml) => {
361
+ writeProject(tmpDir, {private: true, ...rootFields});
362
+ if (yaml) fs.writeFileSync(path.join(tmpDir, 'pnpm-workspace.yaml'), yaml);
363
+ const hoisted = install(tmpDir, '@astryxdesign/core', '0.7.1');
364
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'), DECLARES_CORE);
365
+
366
+ expect(findProjectPackageOf(member, NAMES)).toEqual({name: '@astryxdesign/core', packageDir: hoisted});
367
+ });
368
+
369
+ it.each([
370
+ ['lists other folders', {workspaces: ['packages/*']}],
371
+ ['excludes it with a ! pattern', {workspaces: ['apps/*', '!apps/web']}],
372
+ ['lists other folders in a brace list of globs', {workspaces: ['{packages/*,tools/*}']}],
373
+ ])('stops at the project root when the workspace %s', (_label, rootFields) => {
374
+ writeProject(tmpDir, {private: true, ...rootFields});
375
+ install(tmpDir, '@astryxdesign/core', '0.7.1');
376
+ const project = writeProject(path.join(tmpDir, 'apps', 'web'), DECLARES_CORE);
377
+
378
+ expect(findProjectPackageOf(project, NAMES)).toBeNull();
379
+ });
380
+
381
+ // AST-040 FR11: a Plug'n'Play project that declares Core keeps no
382
+ // node_modules copy; the runtime answers for it.
383
+ it("asks Yarn Plug'n'Play for a project that declares Core and has no node_modules copy", () => {
384
+ const store = writePackage(path.join(tmpDir, 'store', '@astryxdesign-core'), '@astryxdesign/core', '0.7.1');
385
+ const project = withPnp({'@astryxdesign/core': store});
386
+ writeProject(project, DECLARES_CORE);
387
+
388
+ expect(fs.existsSync(path.join(project, 'node_modules', '@astryxdesign'))).toBe(false);
389
+ expect(findProjectPackageOf(project, NAMES)).toEqual({name: '@astryxdesign/core', packageDir: store});
390
+ });
391
+
392
+ // A project nested in another, outside any workspace, has its own install.
393
+ it('stops at the root of a nested project outside any workspace', () => {
394
+ writeProject(tmpDir, {dependencies: {'@astryxdesign/core': '0.8.0'}});
395
+ install(tmpDir, '@astryxdesign/core', '0.8.0');
396
+ const nested = writeProject(path.join(tmpDir, 'apps', 'web'), {dependencies: {'@astryxdesign/core': '0.9.0'}});
397
+
398
+ expect(findProjectPackageOf(nested, NAMES)).toBeNull();
399
+ });
400
+
401
+ // With no Astryx project, released versions read only this folder's own
402
+ // node_modules.
403
+ it('reads only the folder itself when no Astryx project contains it', () => {
404
+ writeProject(tmpDir);
405
+ install(tmpDir, '@astryxdesign/core', '0.7.1');
406
+ const plain = writeProject(path.join(tmpDir, 'plain'));
407
+
408
+ expect(findProjectPackageOf(plain, NAMES)).toBeNull();
409
+ const own = install(plain, '@astryxdesign/core', '0.7.1');
410
+ expect(findProjectPackageOf(plain, NAMES)).toEqual({name: '@astryxdesign/core', packageDir: own});
411
+ });
412
+ });
413
+
414
+ describe('findAstryxProject', () => {
415
+ it('is the nearest folder whose package.json declares an Astryx package', () => {
416
+ writeProject(tmpDir, DECLARES_CORE);
417
+ const tool = writeProject(path.join(tmpDir, 'tools', 'lint'));
418
+ const deep = path.join(tool, 'src');
419
+ fs.mkdirSync(deep, {recursive: true});
420
+
421
+ expect(findAstryxProject(deep)).toBe(tmpDir);
422
+ const app = writeProject(path.join(tmpDir, 'apps', 'web'), {devDependencies: {'@astryxdesign/cli': '0.7.1'}});
423
+ expect(findAstryxProject(app)).toBe(app);
424
+ });
425
+
426
+ it('counts a peer declaration and the legacy scope', () => {
427
+ const integration = writeProject(path.join(tmpDir, 'integration'), {peerDependencies: {'@astryxdesign/core': '^0.7.0'}});
428
+ const legacy = writeProject(path.join(tmpDir, 'legacy'), {dependencies: {'@xds/core': '0.3.0'}});
429
+
430
+ expect(findAstryxProject(integration)).toBe(integration);
431
+ expect(findAstryxProject(legacy)).toBe(legacy);
432
+ });
433
+
434
+ it('stops at a repository root', () => {
435
+ writeProject(tmpDir, DECLARES_CORE);
436
+ const repo = writeProject(path.join(tmpDir, 'repo'));
437
+ fs.mkdirSync(path.join(repo, '.git'));
438
+ const tools = path.join(repo, 'tools');
439
+ fs.mkdirSync(tools, {recursive: true});
440
+
441
+ expect(findAstryxProject(tools)).toBeNull();
442
+ });
443
+
444
+ it('never counts the home folder', () => {
445
+ writeProject(tmpDir, DECLARES_CORE);
446
+ const desktop = path.join(tmpDir, 'Desktop');
447
+ fs.mkdirSync(desktop, {recursive: true});
448
+
449
+ withHome(tmpDir, () => {
450
+ expect(findAstryxProject(desktop)).toBeNull();
451
+ expect(findAstryxProject(tmpDir)).toBeNull();
452
+ });
453
+ });
454
+
455
+ it('returns null when no folder declares Astryx', () => {
456
+ writeProject(tmpDir);
457
+
458
+ expect(findAstryxProject(tmpDir)).toBeNull();
459
+ });
460
+
461
+ it('counts a member its workspace lists when the workspace root declares Astryx', () => {
462
+ writeProject(tmpDir, {private: true, workspaces: ['apps/*'], ...DECLARES_CORE});
463
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'));
464
+ const tests = writeProject(path.join(member, 'e2e'));
465
+
466
+ expect(findAstryxProject(path.join(member, 'src'))).toBe(member);
467
+ expect(findAstryxProject(tests)).toBe(member);
468
+ const other = writeProject(path.join(tmpDir, 'tools', 'lint'));
469
+ expect(findAstryxProject(other)).toBe(tmpDir);
470
+ });
471
+ });
472
+
473
+ describe('findPackageRoot', () => {
474
+ it('finds the nearest package.json at or above the folder', () => {
475
+ writeProject(tmpDir);
476
+ const app = writeProject(path.join(tmpDir, 'apps', 'web'));
477
+ const deep = path.join(app, 'src', 'app');
478
+ fs.mkdirSync(deep, {recursive: true});
479
+
480
+ expect(findPackageRoot(deep)).toBe(app);
481
+ expect(findPackageRoot(tmpDir)).toBe(tmpDir);
482
+ });
483
+
484
+ it('returns null with no package.json', () => {
485
+ expect(findPackageRoot(tmpDir)).toBeNull();
486
+ });
487
+ });
488
+
489
+ describe('findWorkspaceRoot', () => {
490
+ it('finds the nearest folder that declares workspaces in package.json', () => {
491
+ writeProject(tmpDir, {private: true, workspaces: ['apps/*']});
492
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'));
493
+
494
+ expect(findWorkspaceRoot(path.join(member, 'src'))).toBe(tmpDir);
495
+ });
496
+
497
+ it('finds the nearest folder with pnpm-workspace.yaml', () => {
498
+ writeProject(tmpDir);
499
+ fs.writeFileSync(path.join(tmpDir, 'pnpm-workspace.yaml'), "packages:\n - 'apps/*'\n");
500
+ const member = writeProject(path.join(tmpDir, 'apps', 'web'));
501
+
502
+ expect(findWorkspaceRoot(member)).toBe(tmpDir);
503
+ });
504
+
505
+ it('is the nearest package.json outside a workspace, and the folder itself with none', () => {
506
+ const app = writeProject(path.join(tmpDir, 'app'));
507
+ const src = path.join(app, 'src');
508
+ fs.mkdirSync(src, {recursive: true});
509
+
510
+ expect(findWorkspaceRoot(src)).toBe(app);
511
+ expect(findWorkspaceRoot(tmpDir)).toBe(tmpDir);
512
+ });
513
+ });
514
+
515
+ describe('findNodeModulesPackage', () => {
516
+ it('finds a package hoisted to a workspace root and names the folder that holds it', () => {
517
+ const hoisted = install(tmpDir, '@acme/widgets', '1.0.0');
518
+ const member = path.join(tmpDir, 'apps', 'web');
519
+ fs.mkdirSync(member, {recursive: true});
520
+
521
+ expect(findNodeModulesPackage(member, '@acme/widgets')).toEqual({packageDir: hoisted, hostDir: tmpDir});
522
+ });
523
+
524
+ it('does not ask Plug\'n\'Play', () => {
525
+ const store = writePackage(path.join(tmpDir, 'store', 'acme-widgets'), '@acme/widgets');
526
+ const project = withPnp({'@acme/widgets': store});
527
+
528
+ expect(findNodeModulesPackage(project, '@acme/widgets')).toBeNull();
529
+ });
530
+ });
531
+
156
532
  describe('findProjectRoot', () => {
157
533
  it('finds root with workspaces in package.json', () => {
158
534
  fs.writeFileSync(
@@ -0,0 +1,18 @@
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
+ * Whether every recorded path is still in the state it had when it was read.
6
+ * @param {Map<string, string>} recorded - From recordReads.
7
+ * @returns {boolean}
8
+ */
9
+ export function unchangedSince(recorded: Map<string, string>): boolean;
10
+ /**
11
+ * Run `fn`, adding to `seen` each path it reads, with that path's state when
12
+ * it was first read (see the file header).
13
+ * @template T
14
+ * @param {Map<string, string>} seen
15
+ * @param {() => T} fn
16
+ * @returns {T}
17
+ */
18
+ export function recordReads<T>(seen: Map<string, string>, fn: () => T): T;
@@ -0,0 +1,131 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Which files and folders an operation read, so a long-running caller
5
+ * can tell whether its inputs changed since.
6
+ *
7
+ * @input An operation run inside `recordReads(seen, fn)`.
8
+ * @output `seen` gets every file whose content the operation read through fs
9
+ * and every folder it listed or probed, each with its state when first
10
+ * read; `unchangedSince(seen)` says whether all are still in that state.
11
+ * @position Used by search's adapter to reuse a gathered catalog only while
12
+ * it is exact. Outside `recordReads` nothing is recorded and every fs call
13
+ * behaves as before.
14
+ */
15
+
16
+ import {AsyncLocalStorage} from 'node:async_hooks';
17
+ import {createRequire, syncBuiltinESMExports} from 'node:module';
18
+ import * as path from 'node:path';
19
+ import {fileURLToPath} from 'node:url';
20
+
21
+ /**
22
+ * The paths the running operation touched: every file it read and every
23
+ * folder it listed or probed.
24
+ * @type {AsyncLocalStorage<Map<string, string>>}
25
+ */
26
+ const reads = new AsyncLocalStorage();
27
+
28
+ /** node:fs's module object, whose functions every import of it shares. */
29
+ const fs = createRequire(import.meta.url)('node:fs');
30
+
31
+ /** The fs reads whose path argument is recorded. */
32
+ const RECORDED = [
33
+ 'readFileSync',
34
+ 'readdirSync',
35
+ 'statSync',
36
+ 'lstatSync',
37
+ 'existsSync',
38
+ 'realpathSync',
39
+ 'accessSync',
40
+ 'openSync',
41
+ ];
42
+
43
+ /** The reads that see a file's content, so the file itself is checked. */
44
+ const CONTENT_READS = new Set(['readFileSync', 'openSync']);
45
+
46
+ let recording = false;
47
+
48
+ /** fs.statSync as loaded, before installRecorder wraps it. */
49
+ const statSyncRaw = fs.statSync;
50
+
51
+ /**
52
+ * Wrap fs's read calls once so that, inside `reads.run`, each path they are
53
+ * given is added to that run's set. Outside a run they behave exactly as
54
+ * before.
55
+ */
56
+ function installRecorder() {
57
+ if (recording) return;
58
+ recording = true;
59
+ for (const name of RECORDED) {
60
+ const original = fs[name];
61
+ if (typeof original !== 'function') continue;
62
+ /**
63
+ * @this {unknown}
64
+ * @param {...any} args
65
+ */
66
+ const wrapped = function (...args) {
67
+ const seen = reads.getStore();
68
+ const target = args[0];
69
+ if (seen && (typeof target === 'string' || target instanceof URL)) {
70
+ const file =
71
+ typeof target === 'string'
72
+ ? path.resolve(target)
73
+ : fileURLToPath(target);
74
+ // A file read is checked itself; a probe or listing is checked by its
75
+ // folder, whose time changes when an entry is added, removed or
76
+ // renamed.
77
+ const checked =
78
+ CONTENT_READS.has(name) || name === 'readdirSync'
79
+ ? file
80
+ : path.dirname(file);
81
+ // Its state is taken when it is first read, before the read, so a
82
+ // change made while the operation runs shows as a change afterwards.
83
+ if (!seen.has(checked)) seen.set(checked, stateOf(checked));
84
+ }
85
+ return original.apply(this, args);
86
+ };
87
+ Object.assign(wrapped, original);
88
+ fs[name] = wrapped;
89
+ }
90
+ // ESM imports of node:fs see the wrapped functions too.
91
+ syncBuiltinESMExports();
92
+ }
93
+
94
+ /**
95
+ * One path's state: its change time and size, or that it is absent.
96
+ * @param {string} file
97
+ * @returns {string}
98
+ */
99
+ function stateOf(file) {
100
+ try {
101
+ const stat = statSyncRaw(file);
102
+ return `${stat.mtimeMs}\0${stat.size}`;
103
+ } catch {
104
+ return '-';
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Whether every recorded path is still in the state it had when it was read.
110
+ * @param {Map<string, string>} recorded - From recordReads.
111
+ * @returns {boolean}
112
+ */
113
+ export function unchangedSince(recorded) {
114
+ for (const [file, state] of recorded) {
115
+ if (stateOf(file) !== state) return false;
116
+ }
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Run `fn`, adding to `seen` each path it reads, with that path's state when
122
+ * it was first read (see the file header).
123
+ * @template T
124
+ * @param {Map<string, string>} seen
125
+ * @param {() => T} fn
126
+ * @returns {T}
127
+ */
128
+ export function recordReads(seen, fn) {
129
+ installRecorder();
130
+ return reads.run(seen, fn);
131
+ }