@astryxdesign/cli 0.1.2 → 0.1.3

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 (238) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +49 -55
  3. package/docs/layout.doc.dense.mjs +90 -0
  4. package/docs/layout.doc.mjs +160 -0
  5. package/docs/principles.doc.dense.mjs +2 -2
  6. package/docs/principles.doc.mjs +4 -0
  7. package/docs/principles.doc.zh.mjs +2 -2
  8. package/docs/theme.doc.mjs +2 -2
  9. package/package.json +13 -1
  10. package/src/api/blog.mjs +192 -0
  11. package/src/api/blog.test.mjs +144 -0
  12. package/src/api/component.mjs +253 -12
  13. package/src/api/discover.mjs +14 -7
  14. package/src/api/doctor.mjs +10 -25
  15. package/src/api/index.mjs +1 -0
  16. package/src/api/layout.mjs +11 -10
  17. package/src/api/layout.test.mjs +4 -1
  18. package/src/api/template-integration.test.mjs +225 -0
  19. package/src/api/template.mjs +242 -84
  20. package/src/api/validate-integration.mjs +370 -0
  21. package/src/api/validate-integration.test.mjs +222 -0
  22. package/src/codemod.mjs +93 -0
  23. package/src/codemod.test.mjs +134 -0
  24. package/src/codemods/__tests__/registry.test.mjs +1 -0
  25. package/src/codemods/__tests__/runner.test.mjs +103 -0
  26. package/src/codemods/integration-discovery.mjs +168 -0
  27. package/src/codemods/integration-discovery.test.mjs +234 -0
  28. package/src/codemods/integration-runner.mjs +109 -0
  29. package/src/codemods/registry.mjs +1 -0
  30. package/src/codemods/run-codemod.mjs +207 -0
  31. package/src/codemods/runner.mjs +73 -96
  32. package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
  33. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
  34. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
  35. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
  36. package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
  37. package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
  38. package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
  39. package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
  40. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
  41. package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
  42. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
  43. package/src/commands/agent-docs.mjs +4 -1
  44. package/src/commands/blog.mjs +60 -0
  45. package/src/commands/build-theme.import-path.test.mjs +2 -12
  46. package/src/commands/build-theme.mjs +117 -0
  47. package/src/commands/build-theme.prose.test.mjs +5 -15
  48. package/src/commands/build-theme.watch.test.mjs +149 -0
  49. package/src/commands/component/index.mjs +44 -10
  50. package/src/commands/component-ownership.test.mjs +227 -0
  51. package/src/commands/discover.mjs +5 -16
  52. package/src/commands/doctor.test.mjs +3 -3
  53. package/src/commands/ensure-core-built.mjs +120 -0
  54. package/src/commands/json-contract.test.mjs +0 -32
  55. package/src/commands/swizzle.mjs +224 -235
  56. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  57. package/src/commands/swizzle.routing.test.mjs +279 -0
  58. package/src/commands/template.mjs +33 -37
  59. package/src/commands/upgrade.config-ordering.test.mjs +273 -0
  60. package/src/commands/upgrade.integration-policy.test.mjs +204 -0
  61. package/src/commands/upgrade.mjs +349 -170
  62. package/src/commands/validate-integration.mjs +110 -0
  63. package/src/commands/validate-integration.test.mjs +124 -0
  64. package/src/config.mjs +7 -20
  65. package/src/config.test.mjs +89 -13
  66. package/src/index.mjs +8 -3
  67. package/src/integration.mjs +19 -0
  68. package/src/lib/component-discovery.mjs +177 -0
  69. package/src/lib/config-cache.mjs +91 -0
  70. package/src/lib/config-cache.test.mjs +83 -0
  71. package/src/lib/config-schema.mjs +41 -55
  72. package/src/lib/error-codes.mjs +15 -12
  73. package/src/lib/integration-warnings.mjs +62 -0
  74. package/src/lib/integration-warnings.test.mjs +102 -0
  75. package/src/lib/integrations.mjs +92 -115
  76. package/src/lib/integrations.test.mjs +88 -107
  77. package/src/lib/manifest.mjs +5 -2
  78. package/src/lib/module-loader.mjs +80 -0
  79. package/src/lib/module-loader.test.mjs +106 -0
  80. package/src/lib/project.mjs +502 -0
  81. package/src/lib/project.test.mjs +308 -0
  82. package/src/lib/site.mjs +20 -0
  83. package/src/template.mjs +73 -0
  84. package/src/template.test.mjs +127 -0
  85. package/src/types/api.d.ts +2 -6
  86. package/src/types/base.d.ts +3 -9
  87. package/src/types/codemod.d.ts +81 -0
  88. package/src/types/component.d.ts +27 -2
  89. package/src/types/config.d.ts +56 -85
  90. package/src/types/error-codes.d.ts +5 -3
  91. package/src/types/index.d.ts +0 -1
  92. package/src/types/integration.d.ts +29 -0
  93. package/src/types/swizzle.d.ts +9 -2
  94. package/src/types/template-api.d.ts +54 -0
  95. package/src/types/template.d.ts +10 -7
  96. package/src/types/upgrade.d.ts +29 -0
  97. package/src/types/validate-integration.d.ts +24 -0
  98. package/src/utils/github.mjs +0 -237
  99. package/src/utils/interactive.mjs +2 -2
  100. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
  101. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
  102. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
  103. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
  104. package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
  105. package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
  106. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
  107. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
  108. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
  109. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
  110. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
  111. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
  112. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
  113. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
  114. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
  115. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
  116. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
  117. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
  118. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
  119. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
  120. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
  121. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
  122. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
  123. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
  124. package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
  125. package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
  126. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
  127. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
  128. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
  129. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
  130. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
  131. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
  132. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
  133. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
  134. package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
  135. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
  136. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
  137. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
  138. package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
  139. package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
  140. package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
  141. package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
  142. package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
  143. package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
  144. package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
  145. package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
  146. package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
  147. package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
  148. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
  149. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
  150. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  151. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
  152. package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
  153. package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
  154. package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
  155. package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
  156. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
  157. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
  158. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
  159. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
  160. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
  161. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
  162. package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
  163. package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
  164. package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
  165. package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
  166. package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
  167. package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
  168. package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
  169. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
  170. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
  171. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
  172. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
  173. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
  174. package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
  175. package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
  176. package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
  177. package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
  178. package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
  179. package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
  180. package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
  181. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
  182. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
  183. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
  184. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
  185. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
  186. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
  187. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
  188. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
  189. package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
  190. package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
  191. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
  192. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
  193. package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
  194. package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
  195. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
  196. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
  197. package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
  198. package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
  199. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
  200. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
  201. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
  202. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
  203. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
  204. package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
  205. package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
  206. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
  207. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
  208. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
  209. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
  210. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
  211. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
  212. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
  213. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
  214. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
  215. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
  216. package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
  217. package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
  218. package/templates/pages/kanban-board/page.tsx +729 -0
  219. package/templates/pages/kanban-board/template.doc.mjs +12 -0
  220. package/templates/pages/shell-nav/page.tsx +321 -0
  221. package/templates/pages/shell-nav/template.doc.mjs +12 -0
  222. package/templates/pages/shell-side-nav/page.tsx +241 -0
  223. package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
  224. package/templates/pages/shell-top-nav/page.tsx +224 -0
  225. package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
  226. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  227. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  228. package/src/commands/gap-report.mjs +0 -464
  229. package/src/commands/gap-report.test.mjs +0 -168
  230. package/src/commands/swizzle-gap-safety.test.mjs +0 -273
  231. package/src/lib/config.mjs +0 -113
  232. package/src/lib/config.test.mjs +0 -91
  233. package/src/types/gap-report.d.ts +0 -29
  234. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
  235. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
  236. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
  237. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
  238. /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
@@ -0,0 +1,360 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect} from 'vitest';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+
7
+ /**
8
+ * Invoke the transform directly with a configured jscodeshift api, mirroring
9
+ * how other transform tests (and `runConfigCodemod`) drive transforms via the
10
+ * unified `(file, api) => string | null | undefined` contract.
11
+ *
12
+ * @param {string} source
13
+ * @param {string} [filePath]
14
+ * @returns {Promise<string|null>} the rewritten source, or null for no-op
15
+ */
16
+ async function applyTransform(source, filePath = 'astryx.config.mjs') {
17
+ const {default: transform} = await import(
18
+ '../migrate-layout-components-to-experimental.mjs'
19
+ );
20
+ const jscodeshift = (await import('jscodeshift')).default;
21
+ const j = jscodeshift.withParser('babel');
22
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
23
+ return transform({source, path: filePath}, api);
24
+ }
25
+
26
+ describe('migrate-layout-components-to-experimental — meta', () => {
27
+ it('declares itself as a config codemod', async () => {
28
+ const {meta} = await import(
29
+ '../migrate-layout-components-to-experimental.mjs'
30
+ );
31
+ expect(meta.codemodType).toBe('config');
32
+ expect(meta.title).toMatch(/layout\.components/);
33
+ expect(typeof meta.description).toBe('string');
34
+ });
35
+ });
36
+
37
+ describe('migrate-layout-components-to-experimental — string → {from}', () => {
38
+ it('normalizes a string entry to { from } (named import semantics)', async () => {
39
+ const input = `export default {
40
+ layout: {
41
+ components: {
42
+ KpiCard: '@/components/KpiCard',
43
+ },
44
+ },
45
+ };
46
+ `;
47
+ const output = await applyTransform(input);
48
+ expect(output).not.toBeNull();
49
+ // No longer under layout.
50
+ expect(output).not.toMatch(/layout\s*:/);
51
+ // Relocated under experimental.xle.components as { from }.
52
+ expect(output).toContain('experimental');
53
+ expect(output).toContain('xle');
54
+ expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
55
+ });
56
+
57
+ it('normalizes multiple string entries', async () => {
58
+ const input = `export default {
59
+ layout: {
60
+ components: {
61
+ KpiCard: '@/components/KpiCard',
62
+ Banner: '@/components/Banner',
63
+ },
64
+ },
65
+ };
66
+ `;
67
+ const output = await applyTransform(input);
68
+ expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
69
+ expect(output).toMatch(/Banner:\s*\{\s*from:\s*['"]@\/components\/Banner['"]/);
70
+ });
71
+ });
72
+
73
+ describe('migrate-layout-components-to-experimental — object entries carried unchanged', () => {
74
+ it('preserves default: true (default import semantics)', async () => {
75
+ const input = `export default {
76
+ layout: {
77
+ components: {
78
+ Chart: { from: '@/c/Chart', default: true },
79
+ },
80
+ },
81
+ };
82
+ `;
83
+ const output = await applyTransform(input);
84
+ expect(output).toContain("from: '@/c/Chart'");
85
+ expect(output).toContain('default: true');
86
+ expect(output).toContain('experimental');
87
+ expect(output).toContain('xle');
88
+ });
89
+
90
+ it('preserves description', async () => {
91
+ const input = `export default {
92
+ layout: {
93
+ components: {
94
+ Chart: { from: '@/c/Chart', description: 'A chart' },
95
+ },
96
+ },
97
+ };
98
+ `;
99
+ const output = await applyTransform(input);
100
+ expect(output).toContain("from: '@/c/Chart'");
101
+ expect(output).toContain("description: 'A chart'");
102
+ });
103
+
104
+ it('preserves from/description/default and key order', async () => {
105
+ const input = `export default {
106
+ layout: {
107
+ components: {
108
+ Chart: { from: '@/c/Chart', description: 'A chart', default: true },
109
+ },
110
+ },
111
+ };
112
+ `;
113
+ const output = await applyTransform(input);
114
+ // Order: from, then description, then default.
115
+ expect(output).toMatch(
116
+ /from:\s*'@\/c\/Chart',\s*description:\s*'A chart',\s*default:\s*true/,
117
+ );
118
+ });
119
+
120
+ it('migrates a mix of string and object entries', async () => {
121
+ const input = `export default {
122
+ layout: {
123
+ components: {
124
+ KpiCard: '@/components/KpiCard',
125
+ Chart: { from: '@/c/Chart', default: true },
126
+ },
127
+ },
128
+ };
129
+ `;
130
+ const output = await applyTransform(input);
131
+ expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
132
+ expect(output).toContain("from: '@/c/Chart'");
133
+ expect(output).toContain('default: true');
134
+ });
135
+ });
136
+
137
+ describe('migrate-layout-components-to-experimental — wrapper forms', () => {
138
+ it('handles bare object literal default export', async () => {
139
+ const input = `export default {
140
+ layout: { components: { KpiCard: '@/components/KpiCard' } },
141
+ };
142
+ `;
143
+ const output = await applyTransform(input);
144
+ expect(output).toContain('experimental');
145
+ expect(output).toContain('xle');
146
+ });
147
+
148
+ it('handles createConfig({ ... }) wrapper', async () => {
149
+ const input = `import { createConfig } from '@astryxdesign/cli';
150
+
151
+ export default createConfig({
152
+ layout: { components: { KpiCard: '@/components/KpiCard' } },
153
+ });
154
+ `;
155
+ const output = await applyTransform(input);
156
+ expect(output).not.toBeNull();
157
+ expect(output).toContain('createConfig(');
158
+ expect(output).toContain('experimental');
159
+ expect(output).toContain('xle');
160
+ expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
161
+ expect(output).not.toMatch(/layout\s*:/);
162
+ });
163
+ });
164
+
165
+ describe('migrate-layout-components-to-experimental — placement', () => {
166
+ it('adds xle to a pre-existing experimental (no xle)', async () => {
167
+ const input = `export default {
168
+ experimental: { foo: true },
169
+ layout: { components: { KpiCard: '@/components/KpiCard' } },
170
+ };
171
+ `;
172
+ const output = await applyTransform(input);
173
+ expect(output).toContain('foo: true');
174
+ expect(output).toContain('xle');
175
+ expect(output).toMatch(/KpiCard:\s*\{\s*from:/);
176
+ });
177
+ });
178
+
179
+ describe('migrate-layout-components-to-experimental — no-op cases', () => {
180
+ it('returns null when there is no layout', async () => {
181
+ const input = `export default { theme: 'dark' };\n`;
182
+ const output = await applyTransform(input);
183
+ expect(output).toBeNull();
184
+ });
185
+
186
+ it('returns null when layout has no components', async () => {
187
+ const input = `export default { layout: { foo: 1 } };\n`;
188
+ // layout exists but has no components → no-op (return null) BEFORE the
189
+ // "extra key" check, because there is nothing to migrate.
190
+ const output = await applyTransform(input);
191
+ expect(output).toBeNull();
192
+ });
193
+
194
+ it('returns null on an already-migrated config (idempotence-ish)', async () => {
195
+ const input = `export default {
196
+ experimental: { xle: { components: { KpiCard: { from: '@/components/KpiCard' } } } },
197
+ };
198
+ `;
199
+ const output = await applyTransform(input);
200
+ expect(output).toBeNull();
201
+ });
202
+ });
203
+
204
+ describe('migrate-layout-components-to-experimental — bail (throw) cases', () => {
205
+ it('throws when experimental.xle already exists', async () => {
206
+ const input = `export default {
207
+ experimental: { xle: { components: {} } },
208
+ layout: { components: { KpiCard: '@/components/KpiCard' } },
209
+ };
210
+ `;
211
+ await expect(applyTransform(input)).rejects.toThrow(/experimental\.xle/);
212
+ });
213
+
214
+ it('throws when layout has an extra key besides components', async () => {
215
+ const input = `export default {
216
+ layout: {
217
+ components: { KpiCard: '@/components/KpiCard' },
218
+ spacing: 8,
219
+ },
220
+ };
221
+ `;
222
+ await expect(applyTransform(input)).rejects.toThrow(/other than/);
223
+ });
224
+
225
+ it('throws when an entry value is an identifier', async () => {
226
+ const input = `const Kpi = '@/components/KpiCard';
227
+ export default {
228
+ layout: { components: { KpiCard: Kpi } },
229
+ };
230
+ `;
231
+ await expect(applyTransform(input)).rejects.toThrow(/cannot be safely migrated/);
232
+ });
233
+
234
+ it('throws when an entry value is a call expression', async () => {
235
+ const input = `export default {
236
+ layout: { components: { KpiCard: resolve('@/components/KpiCard') } },
237
+ };
238
+ `;
239
+ await expect(applyTransform(input)).rejects.toThrow(/cannot be safely migrated/);
240
+ });
241
+
242
+ it('throws on a non-object default export (variable reference)', async () => {
243
+ const input = `const config = { layout: { components: {} } };
244
+ export default config;
245
+ `;
246
+ await expect(applyTransform(input)).rejects.toThrow(/statically analyze/);
247
+ });
248
+ });
249
+
250
+ describe('migrate-layout-components-to-experimental — end-to-end via runConfigCodemod', () => {
251
+ it('rewrites astryx.config.mjs on disk through the unified runner', async () => {
252
+ const {runConfigCodemod} = await import('../../../run-codemod.mjs');
253
+ const {default: transform, meta} = await import(
254
+ '../migrate-layout-components-to-experimental.mjs'
255
+ );
256
+ const jscodeshift = (await import('jscodeshift')).default;
257
+
258
+ const originalCwd = process.cwd();
259
+ const tmpDir = fs.mkdtempSync(
260
+ path.join(process.cwd(), '.astryx-v013-codemod-test-'),
261
+ );
262
+ try {
263
+ process.chdir(tmpDir);
264
+ fs.writeFileSync(
265
+ path.join(tmpDir, 'package.json'),
266
+ JSON.stringify({name: 'consumer'}),
267
+ );
268
+ fs.writeFileSync(
269
+ path.join(tmpDir, 'astryx.config.mjs'),
270
+ `export default {
271
+ layout: { components: { KpiCard: '@/components/KpiCard' } },
272
+ };
273
+ `,
274
+ );
275
+
276
+ const entry = {
277
+ id: 'migrate-layout-components-to-experimental',
278
+ type: 'config',
279
+ codemod: {title: meta.title, transform},
280
+ package: 'core',
281
+ version: '0.1.3',
282
+ };
283
+ const log = {
284
+ step() {},
285
+ info() {},
286
+ success() {},
287
+ warn() {},
288
+ error() {},
289
+ message() {},
290
+ };
291
+ const result = runConfigCodemod(entry, {apply: true, log, jscodeshift});
292
+
293
+ expect(result.errors).toHaveLength(0);
294
+ expect(result.filesChanged).toBe(1);
295
+ const rewritten = fs.readFileSync(
296
+ path.join(tmpDir, 'astryx.config.mjs'),
297
+ 'utf-8',
298
+ );
299
+ expect(rewritten).toContain('experimental');
300
+ expect(rewritten).toContain('xle');
301
+ expect(rewritten).toMatch(/KpiCard:\s*\{\s*from:/);
302
+ expect(rewritten).not.toMatch(/layout\s*:/);
303
+ } finally {
304
+ process.chdir(originalCwd);
305
+ fs.rmSync(tmpDir, {recursive: true, force: true});
306
+ }
307
+ });
308
+
309
+ it('surfaces a thrown bail as a structured error through runConfigCodemod', async () => {
310
+ const {runConfigCodemod} = await import('../../../run-codemod.mjs');
311
+ const {default: transform, meta} = await import(
312
+ '../migrate-layout-components-to-experimental.mjs'
313
+ );
314
+ const jscodeshift = (await import('jscodeshift')).default;
315
+
316
+ const originalCwd = process.cwd();
317
+ const tmpDir = fs.mkdtempSync(
318
+ path.join(process.cwd(), '.astryx-v013-codemod-test-'),
319
+ );
320
+ try {
321
+ process.chdir(tmpDir);
322
+ fs.writeFileSync(
323
+ path.join(tmpDir, 'package.json'),
324
+ JSON.stringify({name: 'consumer'}),
325
+ );
326
+ fs.writeFileSync(
327
+ path.join(tmpDir, 'astryx.config.mjs'),
328
+ `export default {
329
+ experimental: { xle: { components: {} } },
330
+ layout: { components: { KpiCard: '@/components/KpiCard' } },
331
+ };
332
+ `,
333
+ );
334
+
335
+ const entry = {
336
+ id: 'migrate-layout-components-to-experimental',
337
+ type: 'config',
338
+ codemod: {title: meta.title, transform},
339
+ package: 'core',
340
+ version: '0.1.3',
341
+ };
342
+ const log = {
343
+ step() {},
344
+ info() {},
345
+ success() {},
346
+ warn() {},
347
+ error() {},
348
+ message() {},
349
+ };
350
+ const result = runConfigCodemod(entry, {apply: true, log, jscodeshift});
351
+
352
+ expect(result.filesChanged).toBe(0);
353
+ expect(result.errors).toHaveLength(1);
354
+ expect(result.errors[0].error).toMatch(/experimental\.xle/);
355
+ } finally {
356
+ process.chdir(originalCwd);
357
+ fs.rmSync(tmpDir, {recursive: true, force: true});
358
+ }
359
+ });
360
+ });
@@ -0,0 +1,19 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file v0.1.3 transform manifest
5
+ *
6
+ * Lists all codemods for the v0.1.3 release in the order they should run.
7
+ */
8
+
9
+ import migrateLayoutComponentsToExperimental, {
10
+ meta as migrateLayoutComponentsToExperimentalMeta,
11
+ } from './migrate-layout-components-to-experimental.mjs';
12
+
13
+ export default [
14
+ {
15
+ name: 'migrate-layout-components-to-experimental',
16
+ transform: migrateLayoutComponentsToExperimental,
17
+ meta: migrateLayoutComponentsToExperimentalMeta,
18
+ },
19
+ ];
@@ -0,0 +1,260 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Codemod: migrate astryx.config `layout.components` to
5
+ * `experimental.xle.components`.
6
+ *
7
+ * The published 0.1.2 CLI read XLE app-component registration from
8
+ * `astryx.config.*` under `layout.components`. The next release relocates this
9
+ * to `experimental.xle.components`, validated by a strict schema that REJECTS
10
+ * unknown keys — so any consumer that still has `layout.components` would
11
+ * hard-error on upgrade. This codemod performs the straight relocation.
12
+ *
13
+ * The semantics are IDENTICAL between the old and new shapes; this is purely a
14
+ * MOVE plus a string→object normalization. No meaning is changed:
15
+ *
16
+ * OLD `layout.components` entry forms (from the 0.1.2 shim):
17
+ * - string `'X'` → a NAMED import; the KEY is both the local name and the
18
+ * export name.
19
+ * - object `{ from, description?, default? }` → `default: true` means a
20
+ * default import; otherwise a named import (key = export name).
21
+ *
22
+ * NEW `experimental.xle.components` entry shape (already shipped on the
23
+ * branch; unchanged here): `{ from, description?, default? }`, with the same
24
+ * semantics.
25
+ *
26
+ * So the migration is:
27
+ * - string `'X'` → `{ from: 'X' }`
28
+ * - object `{ from, description?, default? }` → carried over UNCHANGED
29
+ * (from/description/default preserved, key order preserved).
30
+ *
31
+ * The transform recognizes the config default export as EITHER a bare object
32
+ * literal (`export default { ... }`) or a `createConfig({ ... })` call wrapping
33
+ * an object literal (`export default createConfig({ ... })`). Anything else is
34
+ * not statically analyzable and the transform THROWS with a clear
35
+ * migrate-manually message (the unified config-codemod contract has no
36
+ * structured-error return — `runConfigCodemod` surfaces a thrown `err.message`
37
+ * as `{file, codemod, error}`).
38
+ */
39
+
40
+ export const meta = {
41
+ title: 'Migrate astryx.config layout.components to experimental.xle.components',
42
+ description:
43
+ 'Relocates XLE app-component registration from the legacy ' +
44
+ '`layout.components` config key to `experimental.xle.components`. String ' +
45
+ 'entries (`Name: "@/path"`) are normalized to `{ from: "@/path" }` ' +
46
+ '(named import, key = export name); object entries are carried over ' +
47
+ 'unchanged (from/description/default preserved). Bails with a clear ' +
48
+ 'migrate-manually error when the config cannot be statically analyzed or ' +
49
+ 'when merging would be unsafe.',
50
+ codemodType: 'config',
51
+ };
52
+
53
+ const MANUAL =
54
+ 'migrate layout.components to experimental.xle.components manually';
55
+
56
+ /**
57
+ * @param {{source: string, path: string}} file
58
+ * @param {{jscodeshift: Function}} api
59
+ * @returns {string|null}
60
+ */
61
+ export default function transformer(file, api) {
62
+ const j = api.jscodeshift;
63
+ const root = j(file.source);
64
+
65
+ const defaultExports = root.find(j.ExportDefaultDeclaration);
66
+ if (defaultExports.size() === 0) {
67
+ throw new Error(
68
+ `could not statically analyze astryx config (no default export); ${MANUAL}`,
69
+ );
70
+ }
71
+
72
+ const declaration = defaultExports.paths()[0].node.declaration;
73
+
74
+ // Resolve the config object literal from either form:
75
+ // export default { ... }
76
+ // export default createConfig({ ... })
77
+ const configObject = resolveConfigObject(j, declaration);
78
+ if (!configObject) {
79
+ throw new Error(
80
+ `could not statically analyze astryx config; ${MANUAL}`,
81
+ );
82
+ }
83
+
84
+ // Locate the top-level `layout` property and its `components` value.
85
+ const layoutProp = findObjectProperty(j, configObject, 'layout');
86
+ if (!layoutProp) {
87
+ return null; // no layout → nothing to migrate
88
+ }
89
+ if (!j.ObjectExpression.check(layoutProp.value)) {
90
+ throw new Error(
91
+ `astryx config \`layout\` is not an object literal; ${MANUAL}`,
92
+ );
93
+ }
94
+
95
+ const componentsProp = findObjectProperty(j, layoutProp.value, 'components');
96
+ if (!componentsProp) {
97
+ return null; // no layout.components → nothing to migrate
98
+ }
99
+ if (!j.ObjectExpression.check(componentsProp.value)) {
100
+ throw new Error(
101
+ `astryx config \`layout.components\` is not an object literal; ${MANUAL}`,
102
+ );
103
+ }
104
+
105
+ // `components` was the only documented `layout` key. Anything else is
106
+ // unexpected and we cannot safely guess intent.
107
+ const otherLayoutKeys = layoutProp.value.properties.filter(
108
+ p => p !== componentsProp,
109
+ );
110
+ if (otherLayoutKeys.length > 0) {
111
+ throw new Error(
112
+ `astryx config \`layout\` has keys other than \`components\`; ${MANUAL}`,
113
+ );
114
+ }
115
+
116
+ // Build the migrated `components` object literal entry by entry.
117
+ const migratedComponents = buildMigratedComponents(j, componentsProp.value);
118
+
119
+ // Placement into `experimental.xle.components`.
120
+ const experimentalProp = findObjectProperty(j, configObject, 'experimental');
121
+ if (experimentalProp) {
122
+ if (!j.ObjectExpression.check(experimentalProp.value)) {
123
+ throw new Error(
124
+ `astryx config \`experimental\` is not an object literal; ${MANUAL}`,
125
+ );
126
+ }
127
+ const xleProp = findObjectProperty(j, experimentalProp.value, 'xle');
128
+ if (xleProp) {
129
+ // Can't safely merge into a pre-existing experimental.xle.
130
+ throw new Error(
131
+ `astryx config already defines \`experimental.xle\`; ${MANUAL}`,
132
+ );
133
+ }
134
+ experimentalProp.value.properties.push(
135
+ j.objectProperty(j.identifier('xle'), buildXleObject(j, migratedComponents)),
136
+ );
137
+ } else {
138
+ configObject.properties.push(
139
+ j.objectProperty(
140
+ j.identifier('experimental'),
141
+ j.objectExpression([
142
+ j.objectProperty(j.identifier('xle'), buildXleObject(j, migratedComponents)),
143
+ ]),
144
+ ),
145
+ );
146
+ }
147
+
148
+ // Remove the migrated data from `layout`. Since `components` was the only
149
+ // key (asserted above), remove the entire `layout` property.
150
+ configObject.properties = configObject.properties.filter(
151
+ p => p !== layoutProp,
152
+ );
153
+
154
+ return root.toSource();
155
+ }
156
+
157
+ /**
158
+ * Resolve the wrapped config object literal from a default-export declaration.
159
+ * Handles `{ ... }` and `createConfig({ ... })`.
160
+ * @returns {object|null} the ObjectExpression node, or null if not analyzable
161
+ */
162
+ function resolveConfigObject(j, declaration) {
163
+ if (!declaration) return null;
164
+ if (j.ObjectExpression.check(declaration)) {
165
+ return declaration;
166
+ }
167
+ if (
168
+ j.CallExpression.check(declaration) &&
169
+ declaration.arguments.length >= 1 &&
170
+ j.ObjectExpression.check(declaration.arguments[0])
171
+ ) {
172
+ return declaration.arguments[0];
173
+ }
174
+ return null;
175
+ }
176
+
177
+ /**
178
+ * Find a (non-computed, non-spread) property on an object literal by key name.
179
+ * Supports Identifier and string-literal keys.
180
+ * @returns {object|undefined} the property node
181
+ */
182
+ function findObjectProperty(j, objectExpression, name) {
183
+ return objectExpression.properties.find(prop => {
184
+ if (
185
+ !j.ObjectProperty.check(prop) &&
186
+ !j.Property.check(prop)
187
+ ) {
188
+ return false;
189
+ }
190
+ if (prop.computed) return false;
191
+ const key = prop.key;
192
+ if (j.Identifier.check(key)) return key.name === name;
193
+ if (j.StringLiteral.check(key)) return key.value === name;
194
+ if (j.Literal.check(key)) return key.value === name;
195
+ return false;
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Build the migrated `components` ObjectExpression from the old one.
201
+ * @returns {object} ObjectExpression
202
+ */
203
+ function buildMigratedComponents(j, oldComponents) {
204
+ const newProps = oldComponents.properties.map(prop => {
205
+ if (
206
+ (!j.ObjectProperty.check(prop) && !j.Property.check(prop)) ||
207
+ prop.computed
208
+ ) {
209
+ throw new Error(
210
+ `astryx config \`layout.components\` has a non-literal entry; ${MANUAL}`,
211
+ );
212
+ }
213
+
214
+ const value = prop.value;
215
+
216
+ // string `'X'` → `{ from: 'X' }`
217
+ if (j.StringLiteral.check(value) || (j.Literal.check(value) && typeof value.value === 'string')) {
218
+ return j.objectProperty(
219
+ cloneKey(j, prop.key),
220
+ j.objectExpression([
221
+ j.objectProperty(j.identifier('from'), cloneStringLiteral(j, value)),
222
+ ]),
223
+ );
224
+ }
225
+
226
+ // object `{ from, description?, default? }` → carried over UNCHANGED
227
+ if (j.ObjectExpression.check(value)) {
228
+ return j.objectProperty(cloneKey(j, prop.key), value);
229
+ }
230
+
231
+ // anything else (identifier, call, ternary, etc.) → cannot safely migrate
232
+ throw new Error(
233
+ `astryx config \`layout.components\` entry is neither a string nor an ` +
234
+ `object literal and cannot be safely migrated; ${MANUAL}`,
235
+ );
236
+ });
237
+
238
+ return j.objectExpression(newProps);
239
+ }
240
+
241
+ /** Build `{ components: <obj> }` for `experimental.xle`. */
242
+ function buildXleObject(j, migratedComponents) {
243
+ return j.objectExpression([
244
+ j.objectProperty(j.identifier('components'), migratedComponents),
245
+ ]);
246
+ }
247
+
248
+ /** Clone a property key node so we don't reuse a node across the tree. */
249
+ function cloneKey(j, key) {
250
+ if (j.Identifier.check(key)) return j.identifier(key.name);
251
+ if (j.StringLiteral.check(key)) return j.stringLiteral(key.value);
252
+ if (j.Literal.check(key)) return j.literal(key.value);
253
+ return key;
254
+ }
255
+
256
+ /** Build a fresh string literal preserving the original raw quoting if possible. */
257
+ function cloneStringLiteral(j, value) {
258
+ if (j.StringLiteral.check(value)) return j.stringLiteral(value.value);
259
+ return j.literal(value.value);
260
+ }
@@ -180,6 +180,9 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
180
180
  // Rules — the top error-preventers.
181
181
  lines.push('RULES:');
182
182
  lines.push('- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.');
183
+ lines.push('- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (`astryx docs layout`).');
184
+ lines.push('- Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.');
185
+ lines.push('- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.');
183
186
  // Styling guidance tailored to the project's configured system — never
184
187
  // recommend a path that isn't compiled here (xstyle needs the StyleX compiler;
185
188
  // utilities need Tailwind). Tokens are always the source of truth.
@@ -207,7 +210,7 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
207
210
  .sort();
208
211
  if (topics.length > 0) lines.push(` docs <topic> ${topics.join(', ')}`);
209
212
  }
210
- lines.push(' swizzle <Name> eject component source (--gap reports why)');
213
+ lines.push(' swizzle <Name> eject component source for deep customization');
211
214
  lines.push(' upgrade --apply run after any @astryxdesign/core bump');
212
215
  lines.push(MARKER_END);
213
216