@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,109 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Runner for file-based integration codemods.
5
+ *
6
+ * Integration codemods authored with `createCodemod` / `createConfigCodemod`
7
+ * use the file-based contract `(file, api) => string | null | undefined`.
8
+ * Config codemods target the consumer's astryx.config.* file; code codemods
9
+ * are applied to source files discovered under `--path`, filtered by each
10
+ * codemod's `fileExtensions`.
11
+ *
12
+ * Execution is delegated to the shared primitives in `run-codemod.mjs`, which
13
+ * the core registry runner (`runner.mjs`) reuses too — there is a single
14
+ * implementation of "run a config codemod against astryx.config.*" and "run a
15
+ * code codemod against source files".
16
+ */
17
+
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import {
21
+ findSourceFiles,
22
+ makeLog,
23
+ runCodeCodemod,
24
+ runConfigCodemod,
25
+ } from './run-codemod.mjs';
26
+
27
+ /**
28
+ * Run file-based integration codemods, version-ordered. Config codemods run
29
+ * first (targeting astryx.config.*), then code codemods (source globbing).
30
+ *
31
+ * Optional codemods (isOptional) are skipped unless explicitly requested via
32
+ * `codemod` (matched against the codemod id).
33
+ *
34
+ * @param {Array<{version: string, codemods: Array<object>}>} versionGroups
35
+ * @param {object} options
36
+ * @param {boolean} options.apply
37
+ * @param {string} options.path source directory to scan
38
+ * @param {string} [options.codemod] run only this codemod id
39
+ * @param {Set<string>} [options.skipCodemods] codemod ids to exclude
40
+ * @param {Function} options.jscodeshift
41
+ * @param {boolean} [options.silent]
42
+ * @returns {{totalFilesChanged: number, totalTransformsApplied: number, writtenFiles: string[], errors: Array, skippedOptional: Array}}
43
+ */
44
+ export function runIntegrationCodemods(
45
+ versionGroups,
46
+ {apply, path: srcPath, codemod, skipCodemods, jscodeshift, silent = false},
47
+ ) {
48
+ const log = makeLog(silent);
49
+
50
+ let totalFilesChanged = 0;
51
+ let totalTransformsApplied = 0;
52
+ const writtenFiles = [];
53
+ const errors = [];
54
+ const skippedOptional = [];
55
+
56
+ // Flatten and split by type, preserving version ordering.
57
+ const configEntries = [];
58
+ const codeEntries = [];
59
+ for (const {version, codemods} of versionGroups) {
60
+ for (const entry of codemods) {
61
+ const withVersion = {...entry, version};
62
+ // Exclude explicitly skipped codemods (by codemod id).
63
+ if (skipCodemods?.has(entry.id)) continue;
64
+ if (entry.codemod.isOptional && !codemod) {
65
+ skippedOptional.push(withVersion);
66
+ continue;
67
+ }
68
+ if (codemod && entry.id !== codemod) continue;
69
+ if (entry.type === 'config') configEntries.push(withVersion);
70
+ else codeEntries.push(withVersion);
71
+ }
72
+ }
73
+
74
+ // Config codemods first.
75
+ for (const entry of configEntries) {
76
+ log.info(` ${entry.codemod.title} (v${entry.version}, ${entry.package})`);
77
+ const r = runConfigCodemod(entry, {apply, log, jscodeshift});
78
+ totalFilesChanged += r.filesChanged;
79
+ totalTransformsApplied += r.filesChanged;
80
+ writtenFiles.push(...r.writtenFiles);
81
+ errors.push(...r.errors);
82
+ }
83
+
84
+ // Then code codemods (only scan the tree if there are any).
85
+ if (codeEntries.length > 0) {
86
+ const resolvedPath = path.resolve(srcPath);
87
+ const files = fs.existsSync(resolvedPath)
88
+ ? findSourceFiles(resolvedPath)
89
+ : [];
90
+ for (const entry of codeEntries) {
91
+ log.info(
92
+ ` ${entry.codemod.title} (v${entry.version}, ${entry.package})`,
93
+ );
94
+ const r = runCodeCodemod(entry, files, {apply, log, jscodeshift});
95
+ totalFilesChanged += r.filesChanged;
96
+ totalTransformsApplied += r.filesChanged;
97
+ writtenFiles.push(...r.writtenFiles);
98
+ errors.push(...r.errors);
99
+ }
100
+ }
101
+
102
+ return {
103
+ totalFilesChanged,
104
+ totalTransformsApplied,
105
+ writtenFiles,
106
+ errors,
107
+ skippedOptional,
108
+ };
109
+ }
@@ -19,6 +19,7 @@ const registry = new Map([
19
19
  ['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
20
20
  ['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
21
21
  ['0.1.2', () => import('./transforms/v0.1.2/index.mjs')],
22
+ ['0.1.3', () => import('./transforms/v0.1.3/index.mjs')],
22
23
  ]);
23
24
 
24
25
  // Re-export from the shared utility so registry callers and other consumers
@@ -0,0 +1,207 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Shared codemod execution primitives.
5
+ *
6
+ * Both the core registry runner (`runner.mjs` / `upgrade.mjs`) and the
7
+ * integration runner (`integration-runner.mjs`) execute codemods that follow
8
+ * the unified file-based contract:
9
+ *
10
+ * (file, api) => string | null | undefined
11
+ *
12
+ * where `file` is `{path, source}` and `api` is
13
+ * `{jscodeshift, stats, report}`. Config codemods target the consumer's
14
+ * astryx.config.* file; code codemods are applied to source files discovered
15
+ * under `--path`, filtered by each codemod's `fileExtensions`.
16
+ *
17
+ * A codemod ENTRY is normalized to a single shape across both callers:
18
+ *
19
+ * {id, type: 'code' | 'config', codemod: {title, transform, fileExtensions?,
20
+ * isOptional?}, package, version}
21
+ *
22
+ * Integration discovery emits this shape directly. The core registry stores
23
+ * entries as `{name, transform, meta}`; `runner.mjs` normalizes those to this
24
+ * shape at the boundary (see `runner.mjs`).
25
+ *
26
+ * Both kinds reuse the shared output validation from runner.mjs and surface a
27
+ * transform throw as an error (strictness contract).
28
+ */
29
+
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import * as p from '@clack/prompts';
33
+ import {findConfigPath} from '../lib/project.mjs';
34
+ import {fixDirectiveCorruption, validateOutput} from './runner.mjs';
35
+
36
+ export const DEFAULT_CODE_EXTENSIONS = [
37
+ '.tsx',
38
+ '.ts',
39
+ '.jsx',
40
+ '.js',
41
+ '.mjs',
42
+ '.cjs',
43
+ ];
44
+ const PARSEABLE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
45
+
46
+ /**
47
+ * Recursively find candidate source files in a directory.
48
+ * @param {string} dir
49
+ * @returns {string[]}
50
+ */
51
+ export function findSourceFiles(dir) {
52
+ const results = [];
53
+ function walk(currentDir) {
54
+ let entries;
55
+ try {
56
+ entries = fs.readdirSync(currentDir, {withFileTypes: true});
57
+ } catch {
58
+ return;
59
+ }
60
+ for (const entry of entries) {
61
+ const fullPath = path.join(currentDir, entry.name);
62
+ if (entry.isDirectory()) {
63
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
64
+ walk(fullPath);
65
+ } else {
66
+ results.push(fullPath);
67
+ }
68
+ }
69
+ }
70
+ walk(dir);
71
+ return results.sort();
72
+ }
73
+
74
+ /**
75
+ * No-op log surface for silent (`--json`) mode.
76
+ * @param {boolean} silent
77
+ */
78
+ export function makeLog(silent) {
79
+ return silent
80
+ ? {step() {}, info() {}, success() {}, warn() {}, error() {}, message() {}}
81
+ : p.log;
82
+ }
83
+
84
+ /**
85
+ * Apply a config codemod to the consumer's astryx.config.* file.
86
+ *
87
+ * @param {object} entry normalized codemod entry {id, codemod, package}
88
+ * @param {{apply: boolean, log: object, jscodeshift: Function}} ctx
89
+ * @returns {{filesChanged: number, writtenFiles: string[], errors: Array}}
90
+ */
91
+ export function runConfigCodemod(entry, {apply, log, jscodeshift}) {
92
+ const {codemod, id, package: pkg} = entry;
93
+ const name = `${pkg}:${id}`;
94
+ const configPath = findConfigPath(process.cwd());
95
+ if (!configPath) {
96
+ log.info(` ${codemod.title} — no astryx.config.* found; skipping.`);
97
+ return {filesChanged: 0, writtenFiles: [], errors: []};
98
+ }
99
+
100
+ const relativePath = path.relative(process.cwd(), configPath);
101
+ try {
102
+ const source = fs.readFileSync(configPath, 'utf-8');
103
+ const ext = path.extname(configPath);
104
+ const parser = ext === '.tsx' || ext === '.ts' ? 'tsx' : 'babel';
105
+ const j = jscodeshift.withParser(parser);
106
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
107
+ let result = codemod.transform({source, path: configPath}, api);
108
+
109
+ if (result == null || result === source) {
110
+ return {filesChanged: 0, writtenFiles: [], errors: []};
111
+ }
112
+
113
+ result = fixDirectiveCorruption(result);
114
+ const validation = validateOutput(result, source, j, {
115
+ parse: PARSEABLE_EXTENSIONS.includes(ext),
116
+ });
117
+ if (!validation.valid) {
118
+ log.error(` ✗ ${relativePath} — ${validation.reason}`);
119
+ return {
120
+ filesChanged: 0,
121
+ writtenFiles: [],
122
+ errors: [{file: relativePath, codemod: name, error: validation.reason}],
123
+ };
124
+ }
125
+
126
+ if (apply) {
127
+ fs.writeFileSync(configPath, result, 'utf-8');
128
+ log.success(` ✓ ${relativePath}`);
129
+ } else {
130
+ log.warn(` ~ ${relativePath} (would change)`);
131
+ }
132
+ return {
133
+ filesChanged: 1,
134
+ writtenFiles: apply ? [configPath] : [],
135
+ errors: [],
136
+ };
137
+ } catch (err) {
138
+ log.error(` ✗ ${relativePath} — ${err.message}`);
139
+ return {
140
+ filesChanged: 0,
141
+ writtenFiles: [],
142
+ errors: [{file: relativePath, codemod: name, error: err.message}],
143
+ };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Apply a code codemod to discovered source files.
149
+ *
150
+ * @param {object} entry normalized codemod entry {id, codemod, package}
151
+ * @param {string[]} files
152
+ * @param {{apply: boolean, log: object, jscodeshift: Function}} ctx
153
+ * @returns {{filesChanged: number, writtenFiles: string[], errors: Array}}
154
+ */
155
+ export function runCodeCodemod(entry, files, {apply, log, jscodeshift}) {
156
+ const {codemod, id, package: pkg} = entry;
157
+ const name = `${pkg}:${id}`;
158
+ const extensions = new Set(codemod.fileExtensions ?? DEFAULT_CODE_EXTENSIONS);
159
+
160
+ let filesChanged = 0;
161
+ const writtenFiles = [];
162
+ const errors = [];
163
+
164
+ for (const filePath of files) {
165
+ const ext = path.extname(filePath);
166
+ if (!extensions.has(ext)) continue;
167
+
168
+ const relativePath = path.relative(process.cwd(), filePath);
169
+ try {
170
+ const source = fs.readFileSync(filePath, 'utf-8');
171
+ const parser = ext === '.tsx' || ext === '.ts' ? 'tsx' : 'babel';
172
+ const j = jscodeshift.withParser(parser);
173
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
174
+ let result = codemod.transform({source, path: filePath}, api);
175
+
176
+ if (result == null || result === source) continue;
177
+
178
+ result = fixDirectiveCorruption(result);
179
+ const validation = validateOutput(result, source, j, {
180
+ parse: PARSEABLE_EXTENSIONS.includes(ext),
181
+ });
182
+ if (!validation.valid) {
183
+ log.error(` ✗ ${relativePath} — ${validation.reason}`);
184
+ errors.push({
185
+ file: relativePath,
186
+ codemod: name,
187
+ error: validation.reason,
188
+ });
189
+ continue;
190
+ }
191
+
192
+ filesChanged++;
193
+ if (apply) {
194
+ fs.writeFileSync(filePath, result, 'utf-8');
195
+ writtenFiles.push(filePath);
196
+ log.success(` ✓ ${relativePath}`);
197
+ } else {
198
+ log.warn(` ~ ${relativePath} (would change)`);
199
+ }
200
+ } catch (err) {
201
+ log.error(` ✗ ${relativePath} — ${err.message}`);
202
+ errors.push({file: relativePath, codemod: name, error: err.message});
203
+ }
204
+ }
205
+
206
+ return {filesChanged, writtenFiles, errors};
207
+ }
@@ -12,6 +12,7 @@ import * as fs from 'node:fs';
12
12
  import * as path from 'node:path';
13
13
  import * as p from '@clack/prompts';
14
14
  import {humanLog} from '../lib/json.mjs';
15
+ import {runConfigCodemod} from './run-codemod.mjs';
15
16
 
16
17
  // Known corruption patterns that indicate a broken transform.
17
18
  // Each entry: [regex, human-readable description]
@@ -128,83 +129,42 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
128
129
  return {valid: true};
129
130
  }
130
131
 
131
- function isConfigCodemod(transformEntry) {
132
- return transformEntry.meta?.codemodType === 'config';
133
- }
134
-
135
- const CONFIG_CODEMOD_PATHS = new Set([
136
- 'package.json',
137
- 'astryx.config.mjs',
138
- 'xds.config.mjs',
139
- ]);
140
-
141
- function resolveConfigPath(relativePath) {
142
- if (!CONFIG_CODEMOD_PATHS.has(relativePath)) {
143
- throw new Error(`unsupported config codemod path: ${relativePath}`);
144
- }
145
- return path.resolve(process.cwd(), relativePath);
146
- }
147
-
148
- function readOptionalConfigFile(relativePath) {
149
- const fullPath = resolveConfigPath(relativePath);
150
- if (!fs.existsSync(fullPath)) return null;
151
- return {path: relativePath, source: fs.readFileSync(fullPath, 'utf-8')};
152
- }
153
-
154
- function getConfigCodemodContext() {
155
- return {
156
- packageJson: readOptionalConfigFile('package.json'),
157
- astryxConfig: readOptionalConfigFile('astryx.config.mjs'),
158
- xdsConfig: readOptionalConfigFile('xds.config.mjs'),
159
- };
160
- }
161
-
162
- async function runConfigCodemod(transformEntry, {apply, log}) {
163
- const {transform} = transformEntry;
164
- const api = {config: getConfigCodemodContext()};
165
- const result = await transform({path: process.cwd(), source: ''}, api);
166
- const errors = result?.errors ?? [];
167
- if (errors.length > 0) {
168
- for (const error of errors) {
169
- log.error(` ✗ ${error.file ?? 'config'} — ${error.error}`);
170
- }
171
- return {filesChanged: 0, errors};
172
- }
173
-
174
- const changes = result?.changes ?? [];
175
- if (changes.length === 0) return {filesChanged: 0, errors: []};
176
-
177
- for (const change of changes) {
178
- const fullPath = resolveConfigPath(change.path);
179
- if (change.delete) {
180
- if (apply) fs.rmSync(fullPath, {force: true});
181
- log[apply ? 'success' : 'warn'](
182
- ` ${apply ? '✓' : '~'} ${change.path} (delete${apply ? '' : ', dry run'})`,
183
- );
184
- continue;
185
- }
186
-
187
- if (apply) {
188
- fs.writeFileSync(fullPath, change.source, 'utf-8');
189
- }
190
- log[apply ? 'success' : 'warn'](
191
- ` ${apply ? '✓' : '~'} ${change.path}${apply ? '' : ' (would change)'}`,
192
- );
193
- }
194
-
195
- if (changes.length > 0) {
196
- const verb = apply ? 'Updated' : 'Would update';
197
- log.info(
198
- ` ${verb} ${changes.length} config file${changes.length === 1 ? '' : 's'}`,
199
- );
200
- }
201
-
132
+ /**
133
+ * Normalize a core registry transform entry to the unified codemod entry shape
134
+ * consumed by the shared runner (`run-codemod.mjs`).
135
+ *
136
+ * Core registry entries are stored as `{name, transform, meta, optional}`.
137
+ * The shared runner — the same one integration codemods use — operates on
138
+ * `{id, type, codemod: {title, transform, fileExtensions?, isOptional?},
139
+ * package, version}`.
140
+ *
141
+ * CONVENTION — how a core registry entry signals it is a CONFIG codemod:
142
+ * set `meta.codemodType === 'config'` on the entry (the default is a 'code'
143
+ * codemod). A config codemod runs against the consumer's astryx.config.* file
144
+ * via the unified `(file, api)`/jscodeshift contract; a code codemod runs
145
+ * against discovered source files. Any future core config codemod (e.g. a
146
+ * v0.1.3 one) must set `meta.codemodType = 'config'` and author its transform
147
+ * with the same `(file, api) => string | null | undefined` contract used by
148
+ * `createConfigCodemod`.
149
+ *
150
+ * @param {{name: string, transform: Function, meta: object, optional?: boolean}} transformEntry
151
+ * @param {string} version
152
+ * @returns {{id: string, type: 'code'|'config', codemod: object, package: string, version: string}}
153
+ */
154
+ function toUnifiedEntry(transformEntry, version) {
155
+ const {name, transform, meta, optional} = transformEntry;
156
+ const type = meta?.codemodType === 'config' ? 'config' : 'code';
202
157
  return {
203
- filesChanged: changes.length,
204
- writtenFiles: changes
205
- .filter(change => !change.delete && path.extname(change.path) !== '.json')
206
- .map(change => resolveConfigPath(change.path)),
207
- errors: [],
158
+ id: name,
159
+ type,
160
+ codemod: {
161
+ title: meta.title,
162
+ transform,
163
+ fileExtensions: meta.fileExtensions,
164
+ isOptional: !!optional,
165
+ },
166
+ package: 'core',
167
+ version,
208
168
  };
209
169
  }
210
170
 
@@ -216,11 +176,12 @@ async function runConfigCodemod(transformEntry, {apply, log}) {
216
176
  * @param {boolean} options.apply - Write changes to disk
217
177
  * @param {string} options.path - Source directory to scan
218
178
  * @param {string|undefined} options.codemod - Run only this specific transform
179
+ * @param {Set<string>} [options.skipCodemods] - Transform names to exclude
219
180
  * @param {boolean} [options.silent] - Suppress all human-facing output (for --json)
220
181
  */
221
182
  export async function runCodemods(
222
183
  versionManifests,
223
- {apply, path: srcPath, codemod, silent = false},
184
+ {apply, path: srcPath, codemod, skipCodemods, silent = false},
224
185
  ) {
225
186
  // No-op stub object so silent mode skips clack stdout entirely without
226
187
  // littering the body with `if (!silent)` guards.
@@ -233,20 +194,31 @@ export async function runCodemods(
233
194
 
234
195
  const resolvedPath = path.resolve(srcPath);
235
196
 
236
- if (!fs.existsSync(resolvedPath)) {
197
+ // Config codemods target the consumer's astryx.config.* and never read
198
+ // source files, so a missing --path should not block them. Only hard-fail
199
+ // on a missing source path when there is at least one CODE codemod to run.
200
+ const hasCodeCodemod = versionManifests.some(({transforms}) =>
201
+ transforms.some(t => t.meta?.codemodType !== 'config'),
202
+ );
203
+ const sourcePathExists = fs.existsSync(resolvedPath);
204
+
205
+ if (!sourcePathExists && hasCodeCodemod) {
237
206
  log.error(`Source path not found: ${resolvedPath}`);
238
207
  return {ok: false, reason: 'source_path_missing', resolvedPath};
239
208
  }
240
209
 
241
- log.step(`Scanning ${resolvedPath} for source files...`);
242
- const files = findSourceFiles(resolvedPath);
210
+ let files = [];
211
+ if (sourcePathExists) {
212
+ log.step(`Scanning ${resolvedPath} for source files...`);
213
+ files = findSourceFiles(resolvedPath);
243
214
 
244
- if (files.length === 0) {
245
- log.warn('No source files found.');
246
- } else {
247
- log.info(
248
- `Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
249
- );
215
+ if (files.length === 0) {
216
+ log.warn('No source files found.');
217
+ } else {
218
+ log.info(
219
+ `Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
220
+ );
221
+ }
250
222
  }
251
223
 
252
224
  // Dynamically import jscodeshift
@@ -265,6 +237,8 @@ export async function runCodemods(
265
237
  for (const transformEntry of transforms) {
266
238
  // Filter by codemod name if specified
267
239
  if (codemod && transformEntry.name !== codemod) continue;
240
+ // Exclude explicitly skipped codemods (by transform name).
241
+ if (skipCodemods?.has(transformEntry.name)) continue;
268
242
 
269
243
  const {name, transform, meta, optional} = transformEntry;
270
244
  const transformExtensions = new Set(
@@ -279,20 +253,23 @@ export async function runCodemods(
279
253
 
280
254
  log.info(` ${meta.title}`);
281
255
 
282
- if (isConfigCodemod(transformEntry)) {
283
- const result = await runConfigCodemod(transformEntry, {apply, log});
256
+ // Config codemods are routed through the SAME shared runner that
257
+ // integration config codemods use: the transform follows the unified
258
+ // `(file, api)` contract and targets the consumer's astryx.config.*.
259
+ // A core entry signals "config" via `meta.codemodType === 'config'`
260
+ // (see toUnifiedEntry).
261
+ if (meta?.codemodType === 'config') {
262
+ const result = runConfigCodemod(toUnifiedEntry(transformEntry, version), {
263
+ apply,
264
+ log,
265
+ jscodeshift,
266
+ });
284
267
  if (result.errors.length > 0) {
285
- for (const error of result.errors) {
286
- errors.push({
287
- file: error.file ?? 'config',
288
- codemod: name,
289
- error: error.error,
290
- });
291
- }
268
+ errors.push(...result.errors);
292
269
  } else if (result.filesChanged > 0) {
293
270
  totalFilesChanged += result.filesChanged;
294
271
  totalTransformsApplied += result.filesChanged;
295
- writtenFiles.push(...(result.writtenFiles ?? []));
272
+ writtenFiles.push(...result.writtenFiles);
296
273
  }
297
274
  continue;
298
275
  }
@@ -34,10 +34,6 @@ import migrateSelectorChildrenToRenderOption, {
34
34
  meta as migrateSelectorChildrenToRenderOptionMeta,
35
35
  } from './migrate-selector-children-to-render-option.mjs';
36
36
 
37
- import dropXdsPrefixImports, {
38
- meta as dropXdsPrefixImportsMeta,
39
- } from './drop-xds-prefix-imports.mjs';
40
-
41
37
  export default [
42
38
  {
43
39
  name: 'rename-date-picker-to-input',
@@ -75,13 +71,4 @@ export default [
75
71
  transform: migrateSelectorChildrenToRenderOption,
76
72
  meta: migrateSelectorChildrenToRenderOptionMeta,
77
73
  },
78
- {
79
- // XDS-prefix migration (P2380608025). Optional + not tied to a version
80
- // bump: consumers run it explicitly during their migration, e.g.
81
- // astryx upgrade --codemod drop-xds-prefix-imports --codemod-only --apply
82
- name: 'drop-xds-prefix-imports',
83
- transform: dropXdsPrefixImports,
84
- meta: dropXdsPrefixImportsMeta,
85
- optional: true,
86
- },
87
74
  ];
@@ -0,0 +1,67 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect} from 'vitest';
4
+
5
+ async function applyTransform(source, filePath = 'test.css') {
6
+ const {default: transform} = await import('../migrate-xds-css-surfaces.mjs');
7
+ // api.jscodeshift is unused by this CSS codemod, but the runner still
8
+ // passes it — mirror that shape.
9
+ const jscodeshift = (await import('jscodeshift')).default;
10
+ const api = {jscodeshift, stats: () => {}, report: () => {}};
11
+ const file = {source, path: filePath};
12
+ const result = transform(file, api);
13
+ return result ?? source;
14
+ }
15
+
16
+ describe('migrate-xds-css-surfaces', () => {
17
+ it('rewrites the .xds- class-selector prefix', async () => {
18
+ const input = '.xds-heading { color: red; }';
19
+ const output = await applyTransform(input);
20
+ expect(output).toBe('.astryx-heading { color: red; }');
21
+ });
22
+
23
+ it('rewrites the [data-xds-theme] attribute selector', async () => {
24
+ const input = '[data-xds-theme="dark"] .xds-card { background: black; }';
25
+ const output = await applyTransform(input);
26
+ expect(output).toContain('[data-astryx-theme="dark"]');
27
+ expect(output).toContain('.astryx-card');
28
+ expect(output).not.toContain('xds-');
29
+ });
30
+
31
+ it('rewrites data-xds-theme-prose and data-xds-media attribute selectors', async () => {
32
+ const input =
33
+ '[data-xds-theme-prose] {} [data-xds-media="print"] {}';
34
+ const output = await applyTransform(input);
35
+ expect(output).toContain('[data-astryx-theme-prose]');
36
+ expect(output).toContain('[data-astryx-media="print"]');
37
+ });
38
+
39
+ it('rewrites @layer xds-theme and @layer xds-base', async () => {
40
+ const input = '@layer xds-theme, xds-base;\n@layer xds-theme { a { color: red; } }';
41
+ const output = await applyTransform(input);
42
+ expect(output).toContain('astryx-theme');
43
+ expect(output).toContain('astryx-base');
44
+ expect(output).not.toContain('xds-theme');
45
+ expect(output).not.toContain('xds-base');
46
+ });
47
+
48
+ it('does NOT rewrite a bare "xds" in a comment or value', async () => {
49
+ const input = '/* xds tokens live here */\n.card { content: "xds"; }';
50
+ const output = await applyTransform(input);
51
+ expect(output).toBe(input);
52
+ });
53
+
54
+ it('does NOT rewrite a class-like word missing the leading dot', async () => {
55
+ // e.g. inside a JS-ish string or a comment fragment — not a class selector.
56
+ const input = '/* use xds-heading in markup */';
57
+ const output = await applyTransform(input);
58
+ expect(output).toBe(input);
59
+ });
60
+
61
+ it('leaves already-migrated CSS unchanged', async () => {
62
+ const input =
63
+ '@layer astryx-theme { [data-astryx-theme="dark"] .astryx-card {} }';
64
+ const output = await applyTransform(input);
65
+ expect(output).toBe(input);
66
+ });
67
+ });