@astryxdesign/cli 0.6.4-canary.f0355e3 → 0.6.4

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 (331) hide show
  1. package/README.md +96 -99
  2. package/api/build/build.doc.mjs +1 -6
  3. package/api/build/build.test.mjs +0 -22
  4. package/api/build/kit/kit.mjs +5 -44
  5. package/api/component/_adapter.d.mts +0 -25
  6. package/api/component/_adapter.mjs +5 -59
  7. package/api/component/component.d.mts +3 -6
  8. package/api/component/component.doc.mjs +17 -37
  9. package/api/component/component.mjs +9 -249
  10. package/api/component/component.type.d.mts +0 -25
  11. package/api/component/component.type.mjs +0 -44
  12. package/api/discover/_adapter.d.mts +6 -114
  13. package/api/discover/_adapter.mjs +17 -372
  14. package/api/discover/detail/detail.d.mts +6 -18
  15. package/api/discover/detail/detail.mjs +13 -67
  16. package/api/discover/detail/detail.test.mjs +0 -85
  17. package/api/discover/discover.d.mts +9 -3
  18. package/api/discover/discover.doc.mjs +18 -61
  19. package/api/discover/discover.mjs +36 -220
  20. package/api/discover/discover.test.mjs +2 -11
  21. package/api/discover/discover.type.d.mts +8 -147
  22. package/api/discover/discover.type.mjs +12 -102
  23. package/api/discover/list/list.d.mts +6 -20
  24. package/api/discover/list/list.mjs +12 -45
  25. package/api/discover/list/list.test.mjs +0 -46
  26. package/api/discover/search/search.d.mts +16 -18
  27. package/api/discover/search/search.mjs +56 -102
  28. package/api/discover/search/search.test.mjs +10 -144
  29. package/api/docs/_adapter.d.mts +3 -8
  30. package/api/docs/_adapter.mjs +6 -14
  31. package/api/docs/docOverlays.test.mjs +1 -27
  32. package/api/docs/docs.doc.mjs +2 -2
  33. package/api/docs/docs.test.mjs +243 -0
  34. package/api/docs/integration-tree.test.mjs +555 -0
  35. package/api/docs/integrationDocs.test.mjs +314 -0
  36. package/api/doctor/doctor.d.mts +3 -8
  37. package/api/doctor/doctor.doc.mjs +8 -17
  38. package/api/doctor/doctor.mjs +9 -90
  39. package/api/doctor/doctor.test.mjs +10 -122
  40. package/api/doctor/doctor.type.d.mts +1 -1
  41. package/api/doctor/doctor.type.mjs +1 -1
  42. package/api/gap-report/gap-report.doc.mjs +10 -19
  43. package/api/hook/hook.doc.mjs +3 -6
  44. package/api/index.d.mts +2 -1
  45. package/api/index.mjs +5 -5
  46. package/api/init/init.doc.mjs +12 -17
  47. package/api/integration/add-helpers.d.mts +2 -5
  48. package/api/integration/add-helpers.mjs +9 -36
  49. package/api/integration/add-theme.mjs +1 -22
  50. package/api/integration/add-theme.test.mjs +0 -34
  51. package/api/integration/authoring-checks.mjs +2 -2
  52. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  53. package/api/integration/pack-check.mjs +9 -82
  54. package/api/integration/pack-check.test.mjs +0 -90
  55. package/api/integration/pack-check.type.mjs +1 -1
  56. package/api/json/assertResponse.doc.mjs +1 -1
  57. package/api/json/index.ts +1 -0
  58. package/api/json/isError.doc.mjs +1 -1
  59. package/api/layout/_adapter.d.mts +34 -0
  60. package/api/layout/_adapter.mjs +148 -0
  61. package/api/layout/check/check.d.mts +16 -0
  62. package/api/layout/check/check.mjs +40 -0
  63. package/api/layout/expand/expand.d.mts +22 -0
  64. package/api/layout/expand/expand.mjs +155 -0
  65. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  66. package/api/layout/grammar/grammar.d.mts +13 -0
  67. package/api/layout/grammar/grammar.mjs +87 -0
  68. package/api/layout/layout.d.mts +6 -0
  69. package/api/layout/layout.mjs +17 -0
  70. package/api/layout/layout.test.mjs +297 -0
  71. package/api/layout/layout.type.d.mts +89 -0
  72. package/api/layout/layout.type.mjs +103 -0
  73. package/api/layout/layoutCheck.doc.d.mts +11 -0
  74. package/api/layout/layoutCheck.doc.mjs +85 -0
  75. package/api/layout/layoutExpand.doc.d.mts +11 -0
  76. package/api/layout/layoutExpand.doc.mjs +107 -0
  77. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  78. package/api/layout/layoutGrammar.doc.mjs +57 -0
  79. package/api/search/search.d.mts +1 -27
  80. package/api/search/search.doc.mjs +2 -2
  81. package/api/search/search.mjs +16 -228
  82. package/api/search/search.test.mjs +512 -0
  83. package/api/swizzle/swizzle.doc.mjs +5 -7
  84. package/api/template/copy/copy.mjs +1 -1
  85. package/api/template/copy/copy.test.mjs +0 -9
  86. package/api/template/template-integration.test.mjs +65 -1
  87. package/api/template/template.doc.mjs +1 -2
  88. package/api/template/template.mjs +1 -1
  89. package/api/theme/add/add.mjs +25 -17
  90. package/api/theme/add/add.staging.test.mjs +23 -40
  91. package/api/theme/build/build.family.test.mjs +12 -7
  92. package/api/theme/build/build.mjs +18 -8
  93. package/api/theme/generateTonalPalette.doc.mjs +2 -1
  94. package/api/theme/listThemes.doc.mjs +1 -1
  95. package/api/theme/themeAdd.doc.mjs +10 -9
  96. package/api/theme/themeBuild.doc.mjs +13 -13
  97. package/api/theme/themeList.doc.mjs +1 -1
  98. package/api/theme/themeListAvailable.doc.mjs +1 -2
  99. package/api/theme/themePaletteGenerate.doc.mjs +8 -15
  100. package/api/theme/themeTargets.doc.mjs +2 -3
  101. package/api/theme/themeTemplate.doc.mjs +1 -2
  102. package/api/upgrade/run/run.mjs +4 -6
  103. package/api/upgrade/upgrade.doc.mjs +22 -24
  104. package/api/upgrade/upgrade.type.mjs +2 -2
  105. package/assets/codemods/__tests__/runner.test.mjs +1 -3
  106. package/assets/codemods/integration-runner.mjs +3 -3
  107. package/assets/codemods/runner.mjs +4 -5
  108. package/assets/docs/README.md +2 -4
  109. package/assets/docs/browser-support.doc.mjs +11 -11
  110. package/assets/docs/color.doc.mjs +2 -8
  111. package/assets/docs/elevation.doc.mjs +4 -6
  112. package/assets/docs/getting-started.doc.mjs +16 -5
  113. package/assets/docs/icons.doc.mjs +21 -2
  114. package/assets/docs/illustrations.doc.mjs +15 -7
  115. package/assets/docs/internationalization.doc.mjs +5 -7
  116. package/assets/docs/layout.doc.dense.mjs +82 -130
  117. package/assets/docs/layout.doc.mjs +77 -133
  118. package/assets/docs/migration.doc.mjs +21 -19
  119. package/assets/docs/motion.doc.mjs +3 -16
  120. package/assets/docs/principles.doc.dense.mjs +5 -5
  121. package/assets/docs/principles.doc.mjs +0 -8
  122. package/assets/docs/principles.doc.zh.mjs +6 -6
  123. package/assets/docs/shape.doc.mjs +3 -8
  124. package/assets/docs/spacing.doc.mjs +2 -7
  125. package/assets/docs/styling-libraries.doc.mjs +2 -6
  126. package/assets/docs/styling.doc.mjs +23 -19
  127. package/assets/docs/theme.doc.dense.mjs +18 -58
  128. package/assets/docs/theme.doc.mjs +46 -56
  129. package/assets/docs/theme.doc.zh.mjs +8 -9
  130. package/assets/docs/tokens.doc.dense.mjs +2 -2
  131. package/assets/docs/tokens.doc.mjs +8 -389
  132. package/assets/docs/tokens.doc.zh.mjs +2 -2
  133. package/assets/docs/tree/integrations.doc.mjs +451 -25
  134. package/assets/docs/tree/integrations.test.mjs +62 -0
  135. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  136. package/assets/docs/typography.doc.mjs +4 -24
  137. package/assets/docs/working-with-ai.doc.mjs +22 -30
  138. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  139. package/authoring/config/config.doc.mjs +2 -10
  140. package/authoring/config/parse.d.mts +0 -2
  141. package/authoring/config/parse.mjs +0 -19
  142. package/authoring/config/parse.test.mjs +0 -8
  143. package/authoring/config/type.ts +2 -13
  144. package/authoring/doctypes/_schema.d.mts +2 -3
  145. package/authoring/doctypes/_schema.mjs +0 -6
  146. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  147. package/authoring/doctypes/base/type.ts +2 -4
  148. package/authoring/doctypes/command/command.doc.mjs +1 -1
  149. package/authoring/doctypes/command/type.ts +1 -1
  150. package/authoring/doctypes/component/component.doc.mjs +0 -6
  151. package/authoring/doctypes/component/type.ts +0 -8
  152. package/authoring/doctypes/reference/reference.doc.mjs +0 -7
  153. package/authoring/doctypes/reference/type.ts +0 -5
  154. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  155. package/authoring/doctypes/template/template.doc.mjs +1 -1
  156. package/authoring/doctypes/template/type.ts +2 -2
  157. package/authoring/index.d.mts +0 -1
  158. package/authoring/index.d.ts +0 -10
  159. package/authoring/index.mjs +0 -1
  160. package/authoring/integration/integration.doc.mjs +10 -12
  161. package/clients/cli/command-result-coverage.test.mjs +7 -7
  162. package/clients/cli/commands/component/index.mjs +55 -152
  163. package/clients/cli/commands/component-ownership.test.mjs +0 -89
  164. package/clients/cli/commands/component.doc.mjs +9 -27
  165. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  166. package/clients/cli/commands/discover.doc.mjs +9 -53
  167. package/clients/cli/commands/discover.mjs +118 -393
  168. package/clients/cli/commands/docs.doc.mjs +1 -1
  169. package/clients/cli/commands/docs.mjs +17 -60
  170. package/clients/cli/commands/docs.test.mjs +294 -0
  171. package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -3
  172. package/clients/cli/commands/doctor-integration.test.mjs +0 -53
  173. package/clients/cli/commands/doctor.doc.mjs +1 -3
  174. package/clients/cli/commands/doctor.mjs +5 -49
  175. package/clients/cli/commands/gap-report.doc.mjs +9 -10
  176. package/clients/cli/commands/init.doc.mjs +6 -9
  177. package/clients/cli/commands/integration-add.doc.mjs +9 -9
  178. package/clients/cli/commands/integration-authoring.test.mjs +10 -61
  179. package/clients/cli/commands/integration-pack.doc.mjs +9 -5
  180. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  181. package/clients/cli/commands/integration.doc.mjs +4 -4
  182. package/clients/cli/commands/integration.mjs +43 -74
  183. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  184. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  185. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  186. package/clients/cli/commands/layout.doc.mjs +34 -0
  187. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  188. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  189. package/clients/cli/commands/layout.mjs +275 -0
  190. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  191. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  192. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  193. package/clients/cli/commands/manifest.doc.mjs +1 -1
  194. package/clients/cli/commands/search.doc.mjs +3 -10
  195. package/clients/cli/commands/search.mjs +2 -21
  196. package/clients/cli/commands/search.test.mjs +4 -21
  197. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  198. package/clients/cli/commands/template.doc.mjs +1 -1
  199. package/clients/cli/commands/text-json-parity.test.mjs +16 -5
  200. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  201. package/clients/cli/commands/theme-palette-generate.doc.mjs +2 -3
  202. package/clients/cli/commands/theme-palette.doc.mjs +2 -1
  203. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  204. package/clients/cli/commands/theme.doc.mjs +1 -2
  205. package/clients/cli/commands/upgrade.doc.mjs +3 -62
  206. package/clients/cli/index.mjs +10 -28
  207. package/clients/cli/lib/define-command.mjs +4 -28
  208. package/clients/cli/lib/define-command.test.mjs +0 -54
  209. package/clients/cli/lib/exit-codes.test.mjs +9 -18
  210. package/clients/cli/lib/json-shim.mjs +14 -24
  211. package/clients/cli/lib/json-shim.test.mjs +20 -6
  212. package/clients/cli/lib/manifest.mjs +13 -18
  213. package/clients/cli/lib/manifest.test.mjs +2 -5
  214. package/foundation/agent-docs/agent-docs.mjs +1 -1
  215. package/foundation/agent-docs/agent-docs.test.mjs +1159 -0
  216. package/foundation/discovery/authoring-self-docs.mjs +0 -1
  217. package/foundation/discovery/authoring-self-docs.test.mjs +2 -6
  218. package/foundation/discovery/cli-self-docs.mjs +2 -16
  219. package/foundation/discovery/cli-self-docs.test.mjs +0 -20
  220. package/foundation/discovery/docs-discovery.mjs +1 -5
  221. package/foundation/discovery/docs-discovery.test.mjs +0 -21
  222. package/foundation/discovery/docs-section-key.d.mts +1 -1
  223. package/foundation/discovery/docs-section-key.mjs +1 -1
  224. package/foundation/discovery/template-adapter.mjs +1 -1
  225. package/foundation/doc-compiler/doc-loads.test.mjs +14 -3
  226. package/foundation/doc-compiler/tree.d.mts +0 -4
  227. package/foundation/doc-compiler/tree.mjs +1 -6
  228. package/foundation/doc-compiler/tree.test.mjs +598 -0
  229. package/foundation/integrations/cli-requirement.d.mts +6 -26
  230. package/foundation/integrations/cli-requirement.mjs +11 -46
  231. package/foundation/integrations/cli-requirement.test.mjs +2 -7
  232. package/foundation/integrations/contribution-inventory.mjs +1 -1
  233. package/foundation/integrations/integrations.d.mts +1 -14
  234. package/foundation/integrations/integrations.mjs +1 -41
  235. package/foundation/integrations/integrations.test.mjs +0 -31
  236. package/foundation/response/error-codes.doc.mjs +8 -6
  237. package/foundation/response/error-codes.test.mjs +5 -30
  238. package/foundation/response/response-types.doc.d.mts +3 -4
  239. package/foundation/response/response-types.doc.mjs +27 -40
  240. package/foundation/response/response-types.doc.test.mjs +0 -23
  241. package/foundation/response/response.doc.mjs +10 -11
  242. package/foundation/xle/browser.d.mts +3 -3
  243. package/foundation/xle/browser.mjs +3 -3
  244. package/foundation/xle/expand.mjs +2 -2
  245. package/foundation/xle/parse.mjs +1 -1
  246. package/foundation/xle/print.mjs +2 -2
  247. package/foundation/xle/splice.mjs +1 -1
  248. package/package.json +9 -9
  249. package/api/discover/_adapter.test.mjs +0 -215
  250. package/api/discover/_catalog-view.d.mts +0 -115
  251. package/api/discover/_catalog-view.mjs +0 -203
  252. package/api/discover/_catalog-view.test.mjs +0 -128
  253. package/api/discover/detail/item/item.d.mts +0 -26
  254. package/api/discover/detail/item/item.mjs +0 -78
  255. package/api/discover/detail/item/item.test.mjs +0 -73
  256. package/api/integration/pack-check.lifecycle-output.test.mjs +0 -107
  257. package/api/theme/add/add.rollback.test.mjs +0 -158
  258. package/api/theme/build/build.rollback.test.mjs +0 -148
  259. package/api/upgrade/run/files-changed.test.mjs +0 -111
  260. package/assets/codemods/file-count.test.mjs +0 -163
  261. package/assets/docs/tree/add-a-component.doc.mjs +0 -75
  262. package/assets/docs/tree/add-a-theme.doc.mjs +0 -85
  263. package/assets/docs/tree/add-a-topic.doc.mjs +0 -144
  264. package/assets/docs/tree/agent-guidance.doc.mjs +0 -138
  265. package/assets/docs/tree/block-template.doc.mjs +0 -130
  266. package/assets/docs/tree/build-the-template.doc.mjs +0 -28
  267. package/assets/docs/tree/building-blocks.doc.mjs +0 -46
  268. package/assets/docs/tree/check-your-docs.doc.mjs +0 -137
  269. package/assets/docs/tree/checks.doc.mjs +0 -119
  270. package/assets/docs/tree/codemods.doc.mjs +0 -147
  271. package/assets/docs/tree/component-family.doc.mjs +0 -113
  272. package/assets/docs/tree/component-imports.doc.mjs +0 -69
  273. package/assets/docs/tree/component-lookups.doc.mjs +0 -149
  274. package/assets/docs/tree/components.doc.mjs +0 -23
  275. package/assets/docs/tree/configuration.doc.mjs +0 -23
  276. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +0 -182
  277. package/assets/docs/tree/define-the-theme.doc.mjs +0 -118
  278. package/assets/docs/tree/describe-the-component.doc.mjs +0 -57
  279. package/assets/docs/tree/docs.doc.mjs +0 -21
  280. package/assets/docs/tree/document-the-template.doc.mjs +0 -28
  281. package/assets/docs/tree/document-the-theme.doc.mjs +0 -68
  282. package/assets/docs/tree/export-template-assets.doc.mjs +0 -147
  283. package/assets/docs/tree/extend-or-replace.doc.mjs +0 -103
  284. package/assets/docs/tree/fonts-and-assets.doc.mjs +0 -106
  285. package/assets/docs/tree/generate-a-palette.doc.mjs +0 -66
  286. package/assets/docs/tree/grade-template-with-agent.doc.mjs +0 -105
  287. package/assets/docs/tree/help.doc.mjs +0 -16
  288. package/assets/docs/tree/links.doc.mjs +0 -98
  289. package/assets/docs/tree/package-and-test.doc.mjs +0 -32
  290. package/assets/docs/tree/page-template.doc.mjs +0 -71
  291. package/assets/docs/tree/publishing.doc.mjs +0 -111
  292. package/assets/docs/tree/quick-start.doc.mjs +0 -272
  293. package/assets/docs/tree/replace-a-core-component.doc.mjs +0 -104
  294. package/assets/docs/tree/replace-a-core-template.doc.mjs +0 -172
  295. package/assets/docs/tree/sections-and-placement.doc.mjs +0 -108
  296. package/assets/docs/tree/see-it-in-an-app.doc.mjs +0 -59
  297. package/assets/docs/tree/ship.doc.mjs +0 -16
  298. package/assets/docs/tree/short-and-findable.doc.mjs +0 -108
  299. package/assets/docs/tree/single-component.doc.mjs +0 -165
  300. package/assets/docs/tree/start-a-template.doc.mjs +0 -143
  301. package/assets/docs/tree/subcomponent.doc.mjs +0 -115
  302. package/assets/docs/tree/template-assets.doc.mjs +0 -64
  303. package/assets/docs/tree/template-doc-overview.doc.mjs +0 -109
  304. package/assets/docs/tree/template-fonts.doc.mjs +0 -102
  305. package/assets/docs/tree/template-grading-rubric.doc.mjs +0 -452
  306. package/assets/docs/tree/template-icons.doc.mjs +0 -97
  307. package/assets/docs/tree/template-images-media.doc.mjs +0 -127
  308. package/assets/docs/tree/template-styles.doc.mjs +0 -93
  309. package/assets/docs/tree/templates.doc.mjs +0 -34
  310. package/assets/docs/tree/test-in-an-app.doc.mjs +0 -115
  311. package/assets/docs/tree/test-template-in-app.doc.mjs +0 -128
  312. package/assets/docs/tree/themes.doc.mjs +0 -39
  313. package/assets/docs/tree/troubleshooting.doc.mjs +0 -149
  314. package/assets/docs/tree/upgrading.doc.mjs +0 -103
  315. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +0 -51
  316. package/assets/docs/tree/verify-packed-template.doc.mjs +0 -77
  317. package/assets/docs/tree/versioning.doc.mjs +0 -161
  318. package/assets/docs/tree/write-good-templates.doc.mjs +0 -64
  319. package/assets/docs/tree/write-the-template-file.doc.mjs +0 -154
  320. package/authoring/discover/discover.doc.d.mts +0 -13
  321. package/authoring/discover/discover.doc.mjs +0 -138
  322. package/authoring/discover/parse.d.mts +0 -24
  323. package/authoring/discover/parse.mjs +0 -128
  324. package/authoring/discover/parse.test.mjs +0 -124
  325. package/authoring/discover/type.ts +0 -87
  326. package/clients/cli/commands/component-batch.test.mjs +0 -341
  327. package/clients/cli/commands/discover.sources.test.mjs +0 -267
  328. package/clients/cli/commands/integration-verify.doc.mjs +0 -22
  329. package/clients/cli/lib/parse-error-format.test.mjs +0 -81
  330. package/foundation/response/batch.type.d.mts +0 -33
  331. package/foundation/response/batch.type.mjs +0 -34
@@ -0,0 +1,155 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx layout expand` leaf — expand a validated layout expression into
5
+ * XDS TSX (optionally written to disk). Resolution + validation come from
6
+ * ../_adapter.mjs (analyze); this leaf owns block-module assembly, code
7
+ * generation, and the path-safe write, and projects the `layout.expand` envelope.
8
+ *
9
+ * @input expression string (+ options)
10
+ * @output {type:'layout.expand', data}
11
+ * @position api — leaf over ../_adapter.mjs + lib/xle/expand
12
+ */
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import {AstryxError} from '../../error.mjs';
17
+ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
18
+ import {assertWithin, isFilePathArg, PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
19
+ import {detectForm} from '../../../foundation/xle/parse.mjs';
20
+ import {expand} from '../../../foundation/xle/expand.mjs';
21
+ import {stripTemplateAssetRefs} from '../../template/template.mjs';
22
+ import {analyze, formatIssue} from '../_adapter.mjs';
23
+
24
+ /** @param {string} name */
25
+ const normKey = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
26
+
27
+ /**
28
+ * Collect the block names referenced by {hints} anywhere in the doc.
29
+ * @param {import('../../../foundation/xle/xle-ast').XLEDoc} doc
30
+ */
31
+ function collectHintNames(doc) {
32
+ /** @type {Set<string>} */
33
+ const names = new Set();
34
+ /** @param {import('../../../foundation/xle/xle-ast').XLEItem} node */
35
+ const visitNode = (node) => {
36
+ if (!node || node.kind === 'group') {
37
+ (node?.children || []).forEach(visit);
38
+ return;
39
+ }
40
+ if (node.hint?.block) names.add(node.hint.block.name);
41
+ for (const slot of node.slots || []) {
42
+ const value = slot.value;
43
+ if (value && typeof value === 'object' && 'hint' in value && value.hint?.block) {
44
+ names.add(value.hint.block.name);
45
+ }
46
+ if (value && typeof value === 'object' && 'subexpr' in value) {
47
+ (value.subexpr || []).forEach(visit);
48
+ }
49
+ }
50
+ (node.children || []).forEach(visit);
51
+ };
52
+ /** @param {import('../../../foundation/xle/xle-ast').XLEItem} item */
53
+ const visit = (item) => (item?.kind === 'group' ? item.children.forEach(visit) : visitNode(item));
54
+ doc.roots.forEach(visit);
55
+ doc.overlays.forEach(visit);
56
+ return names;
57
+ }
58
+
59
+ /**
60
+ * Build the blockModules map expand() needs: import-mode for app components,
61
+ * splice-mode (reading + asset-stripping the block source) for template blocks.
62
+ * Only blocks actually referenced are read.
63
+ *
64
+ * @param {import('../../../foundation/xle/xle-ast').XLEDoc} doc
65
+ * @param {import('../_adapter.mjs').LayoutBlock[]} blocks
66
+ * @returns {Map<string, import('../../../foundation/xle/xle-ast').BlockModule>}
67
+ */
68
+ function buildBlockModules(doc, blocks) {
69
+ const referenced = collectHintNames(doc);
70
+ if (referenced.size === 0) return new Map();
71
+ const byKey = new Map(blocks.map(b => [normKey(b.dirName), b]));
72
+ /** @type {Map<string, import('../../../foundation/xle/xle-ast').BlockModule>} */
73
+ const modules = new Map();
74
+ for (const name of referenced) {
75
+ const block = byKey.get(normKey(name));
76
+ if (!block) continue;
77
+ if (block.kind === 'component') {
78
+ modules.set(name, /** @type {import('../../../foundation/xle/xle-ast').BlockModule} */ (/** @type {unknown} */ ({mode: 'import', componentName: block.name, importPath: block.importPath, isDefault: block.isDefault})));
79
+ } else if (block.filePath && fs.existsSync(block.filePath)) {
80
+ modules.set(name, /** @type {import('../../../foundation/xle/xle-ast').BlockModule} */ ({mode: 'splice', componentName: block.dirName, source: stripTemplateAssetRefs(fs.readFileSync(block.filePath, 'utf-8'))}));
81
+ }
82
+ }
83
+ return modules;
84
+ }
85
+
86
+ /**
87
+ * `astryx layout expand "<expr>" [path]`
88
+ *
89
+ * @param {string} expression
90
+ * @param {object} [options]
91
+ * @param {string} [options.targetPath] - write TSX here (validated against cwd)
92
+ * @param {'compact'|'outline'|'auto'} [options.form]
93
+ * @param {boolean} [options.loose] - downgrade unknown {hints} to TODO warnings
94
+ * @param {string} [options.name] - generated component name
95
+ * @param {string} [options.cwd]
96
+ * @returns {Promise<import('../layout.type.mjs').LayoutExpandResponse>}
97
+ */
98
+ export async function layoutExpand(expression, options = {}) {
99
+ const {targetPath, form = 'auto', loose = false, name, cwd = process.cwd()} = options;
100
+ const {doc, registry, blocks, errors, warnings} = await analyze(expression, {form, loose, cwd});
101
+
102
+ if (errors.length > 0) {
103
+ throw new AstryxError(
104
+ `Layout expression is invalid:\n` + errors.map(e => ` - ${formatIssue(e)}`).join('\n'),
105
+ errors.flatMap(e => (e.suggestions || []).map(s => ({name: s, reason: 'did you mean this?'}))),
106
+ ERROR_CODES.ERR_LAYOUT_INVALID,
107
+ );
108
+ }
109
+
110
+ const componentName = name || 'GeneratedLayout';
111
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(componentName)) {
112
+ throw new AstryxError(
113
+ `--name must be a PascalCase component name, got '${componentName}'`,
114
+ undefined,
115
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
116
+ );
117
+ }
118
+ const blockModules = buildBlockModules(doc, blocks);
119
+ const result = expand(doc, registry, {componentName, blockModules});
120
+
121
+ let written = null;
122
+ if (targetPath) {
123
+ // The guard sees the file that will be written, not only its directory:
124
+ // a symlink at that name would otherwise carry the write outside the root.
125
+ const fileTarget = isFilePathArg(targetPath)
126
+ ? targetPath
127
+ : path.join(targetPath, `${componentName}.tsx`);
128
+ let filePath;
129
+ try {
130
+ filePath = assertWithin(fileTarget, cwd, {label: 'layout target path'});
131
+ } catch (err) {
132
+ if (err instanceof PathSafetyError) {
133
+ throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
134
+ }
135
+ throw err;
136
+ }
137
+ fs.mkdirSync(path.dirname(filePath), {recursive: true});
138
+ fs.writeFileSync(filePath, result.code);
139
+ written = path.relative(cwd, filePath);
140
+ }
141
+
142
+ return {
143
+ type: 'layout.expand',
144
+ data: {
145
+ form: form === 'auto' ? detectForm(expression) : form,
146
+ code: result.code,
147
+ componentsUsed: result.componentsUsed,
148
+ states: result.states,
149
+ todos: result.todos,
150
+ blocksReferenced: [...blockModules.entries()].map(([name, m]) => ({name, mode: m.mode})),
151
+ warnings: warnings.map(formatIssue),
152
+ written,
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,53 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Path-safety tests for the `layout.expand` write. The file written, not
5
+ * only the directory it lands in, must stay inside the project root.
6
+ */
7
+
8
+ import {describe, it, expect, beforeAll} from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ import * as os from 'node:os';
11
+ import * as path from 'node:path';
12
+ import {layoutExpand} from '../layout.mjs';
13
+ import {buildRegistry} from '../../../foundation/xle/registry.mjs';
14
+
15
+ const SLOW = 30_000;
16
+
17
+ // The registry imports every component doc on first use; warm it once.
18
+ beforeAll(async () => {
19
+ await buildRegistry();
20
+ }, 120_000);
21
+
22
+ describe('layout.expand — write confinement', () => {
23
+ it('rejects a directory target whose generated file is a symlink leading outside cwd', async () => {
24
+ // Inside the workspace so @astryxdesign/core resolves.
25
+ const cwd = fs.mkdtempSync(path.join(process.cwd(), '.xle-confine-test-'));
26
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'xle-confine-outside-'));
27
+ try {
28
+ const victim = path.join(outside, 'victim.tsx');
29
+ fs.writeFileSync(victim, 'OUTSIDE');
30
+ fs.mkdirSync(path.join(cwd, 'out'));
31
+ fs.symlinkSync(victim, path.join(cwd, 'out', 'GeneratedLayout.tsx'));
32
+
33
+ await expect(
34
+ layoutExpand('V > B', {targetPath: './out', cwd}),
35
+ ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
36
+ expect(fs.readFileSync(victim, 'utf-8')).toBe('OUTSIDE');
37
+ } finally {
38
+ fs.rmSync(cwd, {recursive: true, force: true});
39
+ fs.rmSync(outside, {recursive: true, force: true});
40
+ }
41
+ }, SLOW);
42
+
43
+ it('still writes <Name>.tsx into a plain directory target', async () => {
44
+ const cwd = fs.mkdtempSync(path.join(process.cwd(), '.xle-confine-test-'));
45
+ try {
46
+ const result = await layoutExpand('V > B', {targetPath: './out', name: 'Demo', cwd});
47
+ expect(result.data.written).toBe(path.join('out', 'Demo.tsx'));
48
+ expect(fs.existsSync(path.join(cwd, 'out', 'Demo.tsx'))).toBe(true);
49
+ } finally {
50
+ fs.rmSync(cwd, {recursive: true, force: true});
51
+ }
52
+ }, SLOW);
53
+ });
@@ -0,0 +1,13 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * `astryx layout grammar` — the agent cheatsheet, with the alias table
6
+ * generated from this branch's registry (never hand-maintained).
7
+ *
8
+ * @param {{cwd?: string}} [options]
9
+ * @returns {Promise<import('../layout.type.mjs').LayoutGrammarResponse>}
10
+ */
11
+ export function layoutGrammar(options?: {
12
+ cwd?: string;
13
+ }): Promise<import("../layout.type.mjs").LayoutGrammarResponse>;
@@ -0,0 +1,87 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx layout grammar` leaf — the agent cheatsheet, with the alias
5
+ * table generated from this branch's registry (never hand-maintained). Reads
6
+ * only the registry; shares nothing with expand/check, so it has no adapter.
7
+ *
8
+ * @output {type:'layout.grammar', data}
9
+ * @position api — leaf over lib/xle/registry
10
+ */
11
+
12
+ import {buildRegistry} from '../../../foundation/xle/registry.mjs';
13
+ import {MAX_REPEAT} from '../../../foundation/xle/expand.mjs';
14
+
15
+ /**
16
+ * `astryx layout grammar` — the agent cheatsheet, with the alias table
17
+ * generated from this branch's registry (never hand-maintained).
18
+ *
19
+ * @param {{cwd?: string}} [options]
20
+ * @returns {Promise<import('../layout.type.mjs').LayoutGrammarResponse>}
21
+ */
22
+ export async function layoutGrammar(options = {}) {
23
+ const {cwd = process.cwd()} = options;
24
+ const registry = await buildRegistry({cwd});
25
+
26
+ /** @type {string[]} */
27
+ const aliasLines = [];
28
+ /** @type {Map<string, string[]>} */
29
+ const byTarget = new Map();
30
+ for (const [alias, target] of registry.aliases) {
31
+ if (!byTarget.has(target)) byTarget.set(target, []);
32
+ byTarget.get(target)?.push(alias);
33
+ }
34
+ for (const [target, aliases] of [...byTarget.entries()].sort(([a], [b]) => a.localeCompare(b))) {
35
+ aliasLines.push(`${aliases.join('/')}=${target}`);
36
+ }
37
+
38
+ const text = `XLE/XLO — XDS layout expressions (branch-generated; aliases reflect this install)
39
+
40
+ WORKFLOW
41
+ astryx layout check "<expr>" validate; echoes canonical compact + outline forms
42
+ astryx layout expand "<expr>" [path] emit validated TSX (path optional; --name <Pascal>)
43
+ Errors carry line/col + suggestions. Fix and resubmit; nothing is guessed.
44
+
45
+ TWO SURFACES, ONE LANGUAGE (autodetected; --form to force)
46
+ compact: A[cp6 @topNav=TN] > L > LC > S[p6] > (C{card-callout}*4) + T
47
+ outline: indentation = nesting · same-indent = siblings · "repeat N:" block = (...)*N
48
+ slot lines: topNav: TN (or a block: topNav:\\n TN ...)
49
+
50
+ NODE ANATOMY Name#id.enum"payload"[attrs]{hint}*N > children
51
+ .enum unique enum value of any prop: Bd.success Tx.lg B.primary
52
+ "payload" primary text prop (label/title/heading) or text child: TI"Email" B"Save"
53
+ {hint} kebab-case template/component reference (see TEMPLATE REFERENCING) — NEVER text
54
+ *N / xN repeat, at most ${MAX_REPEAT} copies (use $ for the counter: Tk"item-$"*3)
55
+ trailing ! initial selection for scaffolded state: Tab"Overview"!
56
+
57
+ ATTRS [...] (outline: bare tokens after the name, no brackets)
58
+ fused p6 g4 c4 w240 h2 cp2 mw960 rg2 cg2 (per-component: padding lives on Card/Section/AppShell.cp — p6 on AppShell/Layout/VStack errors with a correction)
59
+ key=value t=email href='/x' c{min:340} dv=[top,bottom] — keys validated per component
60
+ flags req opt dis striped hover divider … (isX/hasX props) · negate: !scroll
61
+ align j= main axis, a= cross axis — expander picks hAlign/vAlign per stack direction
62
+ slots @slotName=Node | @slotName=(sub > expr) | @slotName='text' | @slotName=#id
63
+ trigger opens=#id (a plain attr, no @ — binds an onClick that opens the overlay)
64
+ fill on a stack child → wraps in <StackItem size="fill">
65
+
66
+ TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches past the @astryxdesign/core shell)
67
+ C{card-callout} splice a template block (astryx template --list --type block):
68
+ the block is co-defined once in the file, referenced, imports merged
69
+ {kpi-card} standalone reference (no wrapper element) — place a component directly
70
+ {kpi-card}*4 repeat a reference; the definition/import is emitted once
71
+ app components register local ones in astryx.config.mjs to import them by name:
72
+ export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}}}}}
73
+ then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
74
+
75
+ STRUCTURE THE EXPANDER HANDLES
76
+ Layout > LH + LC + LF + LP children auto-route into header/content/footer/start slots
77
+ T > (TR>THC*4) + (TR>TC*4)*6 rows partition into TableHeader/TableBody automatically
78
+ TabList/inputs required value+onChange scaffold typed useState automatically
79
+ overlays compact: tree ;; Dlg#confirm[...] · outline: overlays: section
80
+ trigger: B"Delete"[opens=#confirm]
81
+
82
+ ALIASES (full component names always valid; XDS prefix optional)
83
+ ${aliasLines.join(' ')}
84
+ `;
85
+
86
+ return {type: 'layout.grammar', data: {text, aliases: Object.fromEntries(registry.aliases)}};
87
+ }
@@ -0,0 +1,6 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ export { layoutExpand } from "./expand/expand.mjs";
5
+ export { layoutCheck } from "./check/check.mjs";
6
+ export { layoutGrammar } from "./grammar/grammar.mjs";
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `layout` command barrel — re-exports the expand/check/grammar leaves so
5
+ * the CLI (cli/commands/layout.mjs) and scripted callers import from one place.
6
+ * Each leaf is also importable directly (e.g. api/layout/expand/expand.mjs). The
7
+ * shared analysis core lives in ./_adapter.mjs. `layout` has real subcommands,
8
+ * so there is no flag-dispatch here — the CLI calls the leaf it wants.
9
+ *
10
+ * @input layout expressions (+ options), per leaf
11
+ * @output {type, data} envelopes: layout.expand / layout.check / layout.grammar
12
+ * @position api — command barrel over the expand/check/grammar leaves
13
+ */
14
+
15
+ export {layoutExpand} from './expand/expand.mjs';
16
+ export {layoutCheck} from './check/check.mjs';
17
+ export {layoutGrammar} from './grammar/grammar.mjs';
@@ -0,0 +1,297 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Integration tests for the layout API against the real @astryxdesign/core
5
+ * registry and real template blocks. Every expansion is additionally
6
+ * checked for TSX syntactic validity with the TypeScript parser, so the
7
+ * "expansion emits compilable JSX" contract is enforced, not assumed.
8
+ */
9
+
10
+ import {describe, it, expect, beforeAll} from 'vitest';
11
+ import {mkdtempSync, writeFileSync, rmSync} from 'node:fs';
12
+ import {tmpdir} from 'node:os';
13
+ import {join} from 'node:path';
14
+ import ts from 'typescript';
15
+ import {layoutExpand, layoutCheck, layoutGrammar} from './layout.mjs';
16
+ import {buildRegistry} from '../../foundation/xle/registry.mjs';
17
+
18
+ // The registry imports ~140 .doc.mjs modules on first use; under full-suite
19
+ // parallel load that can exceed the default 5s test timeout. Warm it once.
20
+ beforeAll(async () => {
21
+ await buildRegistry();
22
+ }, 120_000);
23
+
24
+ const SLOW = 30_000;
25
+
26
+ /** Assert TSX parses cleanly; returns the source file for inspection. */
27
+ function expectValidTsx(code) {
28
+ const sourceFile = ts.createSourceFile('generated.tsx', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
29
+ const diagnostics = sourceFile.parseDiagnostics || [];
30
+ const messages = diagnostics.map(d => ts.flattenDiagnosticMessageText(d.messageText, ' '));
31
+ expect(messages).toEqual([]);
32
+ return sourceFile;
33
+ }
34
+
35
+ const LOGIN_COMPACT =
36
+ 'Ctr[h="100dvh"] > C[w=400 p8] > V[g6] > ' +
37
+ '(V[g1] > Tx"Welcome back" + Tx[t=supporting]"Sign in to your account") + ' +
38
+ '(F > TI"Email"[t=email req] + TI"Password"[t=password req]) + ' +
39
+ '(H[j=between a=center] > CB"Remember me" + Lk[href="/forgot"]"Forgot password?") + ' +
40
+ 'B.primary"Sign in"';
41
+
42
+ const CHAT_OUTLINE = `
43
+ AppShell
44
+ topNav: TN
45
+ Layout > LC !scroll
46
+ ChL
47
+ composer:
48
+ ChC
49
+ ChML
50
+ ChS "Today"
51
+ repeat 2:
52
+ ChM
53
+ ChB "Reply $"
54
+ Tbar "Actions"
55
+ B "Delete" opens=#confirm
56
+
57
+ overlays:
58
+ AD#confirm "Delete item?"
59
+ `;
60
+
61
+ const DASHBOARD_COMPACT =
62
+ 'A[@topNav=TN @sideNav=SN] > L > V[g6] > ' +
63
+ '(G[c4 g4] > C{card-callout}*4) + ' +
64
+ '(C[p0] > T[striped] > (TR > THC"Name" + THC"Amount") + (TR > TC"Order $" + TC"\\$12.00")*3)';
65
+
66
+ describe('layoutExpand', () => {
67
+ it('expands the login card with typed state scaffolds', async () => {
68
+ const result = await layoutExpand(LOGIN_COMPACT);
69
+ expect(result.type).toBe('layout.expand');
70
+ const {code} = result.data;
71
+ expectValidTsx(code);
72
+
73
+ expect(code).toContain(`const [email, setEmail] = useState('');`);
74
+ expect(code).toContain(`const [rememberMe, setRememberMe] = useState(false);`);
75
+ expect(code).toContain('<XDSCenter height="100dvh">');
76
+ expect(code).toContain('variant="primary"');
77
+ // axis-neutral j/a resolved onto the HStack's real props
78
+ expect(code).toContain('hAlign="between"');
79
+ expect(code).toContain('vAlign="center"');
80
+ // payload routed to label props, not duplicated as children
81
+ expect(code).not.toMatch(/label="Forgot password\?"[\s\S]{0,80}Forgot password\?/);
82
+ }, SLOW);
83
+
84
+ it('expands the outline chat page: slots, repeat blocks, overlays, triggers', async () => {
85
+ const result = await layoutExpand(CHAT_OUTLINE);
86
+ const {code, form} = result.data;
87
+ expect(form).toBe('outline');
88
+ expectValidTsx(code);
89
+
90
+ expect(code).toContain('composer={');
91
+ // slot satisfied required prop — no double assignment, no TODO for it
92
+ expect(code.match(/composer=/g)).toHaveLength(1);
93
+ expect(code).toContain('TODO(xle): open #confirm');
94
+ expect(code).toContain('overlays — wire open state');
95
+ expect((code.match(/<XDSChatMessage\b/g) || [])).toHaveLength(2);
96
+ }, SLOW);
97
+
98
+ it('expands the dashboard: table partition, repeats with $ counter and \\$ escape', async () => {
99
+ const result = await layoutExpand(DASHBOARD_COMPACT);
100
+ const {code} = result.data;
101
+ expectValidTsx(code);
102
+
103
+ expect(code).toContain('<XDSTableHeader>');
104
+ expect(code).toContain('<XDSTableBody>');
105
+ expect(code).toContain('Order 1');
106
+ expect(code).toContain('Order 3');
107
+ expect(code).toContain('$12.00');
108
+ expect(code).not.toContain('112.00');
109
+ // {card-callout} now splices: the block is co-defined once and referenced
110
+ // four times — no TODO marker.
111
+ expect((code.match(/^function CardCallout\(\)/gm) || [])).toHaveLength(1);
112
+ expect((code.match(/<CardCallout \/>/g) || [])).toHaveLength(4);
113
+ expect(code).not.toContain("TODO(xle): content block 'CardCallout'");
114
+ }, SLOW);
115
+
116
+ it('compact and outline surfaces expand to identical TSX', async () => {
117
+ const check = await layoutCheck(LOGIN_COMPACT);
118
+ const fromCompact = await layoutExpand(LOGIN_COMPACT);
119
+ const fromOutline = await layoutExpand(check.data.outline, {form: 'outline'});
120
+ expect(fromOutline.data.code).toEqual(fromCompact.data.code);
121
+ }, SLOW);
122
+
123
+ it('expansion is deterministic', async () => {
124
+ const a = await layoutExpand(DASHBOARD_COMPACT);
125
+ const b = await layoutExpand(DASHBOARD_COMPACT);
126
+ expect(a.data.code).toEqual(b.data.code);
127
+ }, SLOW);
128
+
129
+ it('throws structured errors with suggestions on invalid expressions', async () => {
130
+ await expect(layoutExpand('A[p6] > Grdi')).rejects.toMatchObject({
131
+ code: 'ERR_LAYOUT_INVALID',
132
+ message: expect.stringMatching(/AppShell has no prop 'padding'/),
133
+ });
134
+ });
135
+
136
+ it('rejects non-PascalCase --name', async () => {
137
+ await expect(layoutExpand('V > C', {name: 'not pascal'})).rejects.toMatchObject({
138
+ code: 'ERR_INVALID_ARGUMENT',
139
+ });
140
+ });
141
+
142
+ it('surfaces parse errors with positions', async () => {
143
+ await expect(layoutExpand('V > > C')).rejects.toMatchObject({
144
+ code: 'ERR_LAYOUT_PARSE',
145
+ message: expect.stringMatching(/line 1/),
146
+ });
147
+ });
148
+
149
+ it('maps pathologically deep nesting to a coded parse error, not ERR_UNKNOWN', async () => {
150
+ await expect(layoutCheck('V > '.repeat(2000) + 'C')).rejects.toMatchObject({
151
+ code: 'ERR_LAYOUT_PARSE',
152
+ });
153
+ });
154
+
155
+ it('wraps top-level repeats and groups in a fragment (valid JSX)', async () => {
156
+ for (const expr of ['B"Sign in"*3', '(B"a" + B"b")', '(B"a" + B"b")*2']) {
157
+ const {data} = await layoutExpand(expr);
158
+ expectValidTsx(data.code);
159
+ expect(data.code).toContain('<>');
160
+ }
161
+ }, SLOW);
162
+
163
+ it('wraps a top-level outline repeat block in a fragment', async () => {
164
+ const {data} = await layoutExpand('repeat 3:\n B "x"', {form: 'outline'});
165
+ expectValidTsx(data.code);
166
+ }, SLOW);
167
+
168
+ it('emits JSX-safe TSX for text payloads containing < and >', async () => {
169
+ const result = await layoutExpand('Text"5 < 3 and 3 > 1"');
170
+ expectValidTsx(result.data.code);
171
+ }, SLOW);
172
+
173
+ it('emits JSX-safe TSX for text payloads containing { and }', async () => {
174
+ const result = await layoutExpand('Text"cost is {price}"');
175
+ expectValidTsx(result.data.code);
176
+ }, SLOW);
177
+
178
+ it('emits JSX-safe TSX when a text payload looks like a closing tag', async () => {
179
+ const result = await layoutExpand('Text"end</Text><img/>"');
180
+ expectValidTsx(result.data.code);
181
+ }, SLOW);
182
+
183
+ it('emits JSX-safe TSX for JSX-special chars in outline form', async () => {
184
+ const result = await layoutExpand('VStack\n Text "a < b"', {form: 'outline'});
185
+ expectValidTsx(result.data.code);
186
+ }, SLOW);
187
+ });
188
+
189
+ describe('layoutCheck', () => {
190
+ it('returns both canonical surfaces for valid input', async () => {
191
+ const result = await layoutCheck('V[g6] > C{card-callout}*2');
192
+ expect(result.data.valid).toBe(true);
193
+ expect(result.data.compact).toContain('{card-callout}');
194
+ expect(result.data.outline).toContain('C {card-callout} x2');
195
+ });
196
+
197
+ it('collects all errors instead of stopping at the first', async () => {
198
+ const result = await layoutCheck('A[p6] > V[g7] > Bd.sucess"x" + C{not-a-block}');
199
+ expect(result.data.valid).toBe(false);
200
+ expect(result.data.errors.length).toBeGreaterThanOrEqual(4);
201
+ const all = result.data.errors.map(e => e.message).join('\n');
202
+ expect(all).toMatch(/no prop 'padding'/);
203
+ expect(all).toMatch(/must be one of/);
204
+ expect(all).toMatch(/Unknown block/);
205
+ });
206
+ });
207
+
208
+ describe('template referencing', () => {
209
+ it('splices a template block: co-defined once, referenced, imports merged', async () => {
210
+ const result = await layoutExpand('S[p6] > C{card-callout}*3', {name: 'Demo'});
211
+ const {code} = result.data;
212
+ expectValidTsx(code);
213
+ // co-defined exactly once, referenced three times, no TODO
214
+ expect((code.match(/^function CardCallout\(\)/gm) || [])).toHaveLength(1);
215
+ expect((code.match(/<CardCallout \/>/g) || [])).toHaveLength(3);
216
+ expect(code).not.toContain('TODO(xle)');
217
+ // the block's own import is hoisted, and shared specifiers dedupe
218
+ expect((code.match(/from '@astryxdesign\/core\/Card'/g) || [])).toHaveLength(1);
219
+ // the block lost its export — it's a local declaration now
220
+ expect(code).not.toContain('export default function CardCallout');
221
+ expect(result.data.blocksReferenced).toEqual([{name: 'CardCallout', mode: 'splice'}]);
222
+ }, SLOW);
223
+
224
+ it('merges a stateful block useState into a single react import', async () => {
225
+ const result = await layoutExpand(
226
+ 'V > TI"Search"[t=text] + {table-column-settings-table}',
227
+ {name: 'Demo'},
228
+ );
229
+ const {code} = result.data;
230
+ expectValidTsx(code);
231
+ // page TextInput scaffolds useState; block also uses useState → one import
232
+ expect((code.match(/^import \{useState\} from 'react';$/gm) || [])).toHaveLength(1);
233
+ expect(code).toContain('function TableColumnSettingsTable()');
234
+ }, SLOW);
235
+
236
+ it('imports app-registered local components (the local-component bridge)', async () => {
237
+ // Inside the workspace so @astryxdesign/core resolves; cleaned up after.
238
+ // A package.json beside the config makes Project.load resolve it as the
239
+ // sibling-of-nearest-package.json (the standard config resolution).
240
+ const cwd = mkdtempSync(join(process.cwd(), '.xle-imp-test-'));
241
+ try {
242
+ writeFileSync(join(cwd, 'package.json'), '{"name": "xle-imp-fixture"}\n');
243
+ writeFileSync(
244
+ join(cwd, 'astryx.config.mjs'),
245
+ `export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}, TimeRangePicker: {from: '@/components/TimeRangePicker'}}}}};\n`,
246
+ );
247
+ const result = await layoutExpand('S[p6] > (G[c4 g4] > {kpi-card}*4) + {time-range-picker}', {
248
+ name: 'Demo',
249
+ cwd,
250
+ });
251
+ const {code} = result.data;
252
+ expectValidTsx(code);
253
+ expect(code).toContain("import {KpiCard} from '@/components/KpiCard';");
254
+ expect(code).toContain("import {TimeRangePicker} from '@/components/TimeRangePicker';");
255
+ expect((code.match(/<KpiCard \/>/g) || [])).toHaveLength(4);
256
+ expect(code).toContain('<TimeRangePicker />');
257
+ expect(result.data.blocksReferenced).toContainEqual({name: 'KpiCard', mode: 'import'});
258
+ } finally {
259
+ rmSync(cwd, {recursive: true, force: true});
260
+ }
261
+ }, SLOW);
262
+
263
+ it('parses a standalone {block} in both surfaces', async () => {
264
+ const check = await layoutCheck('G[c4 g4] > {card-callout}*2');
265
+ expect(check.data.valid).toBe(true);
266
+ expect(check.data.compact).toContain('{card-callout}*2');
267
+ expect(check.data.outline).toMatch(/\{card-callout\} x2/);
268
+ });
269
+ });
270
+
271
+ describe('layoutGrammar', () => {
272
+ it('emits the cheatsheet with branch-generated aliases', async () => {
273
+ const result = await layoutGrammar();
274
+ expect(result.data.text).toContain('TWO SURFACES');
275
+ expect(result.data.aliases.V).toBe('VStack');
276
+ expect(result.data.aliases.TB).toBe('TableBody');
277
+ // every alias target must exist — the table is registry-filtered
278
+ expect(Object.values(result.data.aliases)).not.toContain(undefined);
279
+ });
280
+ });
281
+
282
+
283
+ describe('layout — input validation (API matches CLI)', () => {
284
+ it('rejects an invalid --form value instead of silently parsing as compact', async () => {
285
+ await expect(layoutCheck('V > C', {form: /** @type {any} */ ('xml')})).rejects.toMatchObject({
286
+ code: 'ERR_INVALID_OPTION',
287
+ });
288
+ await expect(layoutExpand('V > C', {form: /** @type {any} */ ('nonsense')})).rejects.toMatchObject({
289
+ code: 'ERR_INVALID_OPTION',
290
+ });
291
+ });
292
+
293
+ it('rejects an empty expression at the API layer', async () => {
294
+ await expect(layoutCheck('')).rejects.toMatchObject({code: 'ERR_INVALID_ARGUMENT'});
295
+ await expect(layoutExpand(' ')).rejects.toMatchObject({code: 'ERR_INVALID_ARGUMENT'});
296
+ });
297
+ });