@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,110 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx validate-integration` command — validate ONE integration
5
+ * package's manifest and contributions and report findings using the
6
+ * AstryxIntegrationIssue model.
7
+ *
8
+ * astryx validate-integration validate the local package (cwd)
9
+ * astryx validate-integration <pkg> validate an installed package
10
+ *
11
+ * Exit code is the contract: 0 when there are no severity:'error' issues
12
+ * (warnings are fine), 1 when any error issue is present — so it works as a CI
13
+ * gate. The no-arg + no-local-manifest case prints guidance and exits 0 (not
14
+ * an integration package is not a failure).
15
+ */
16
+
17
+ import {jsonOut, humanLog} from '../lib/json.mjs';
18
+ import {
19
+ validateLocalIntegration,
20
+ validateInstalledIntegration,
21
+ summarizeIssues,
22
+ } from '../api/validate-integration.mjs';
23
+
24
+ /**
25
+ * Render a validation result for humans.
26
+ * @param {import('../api/validate-integration.mjs').ValidateResult} result
27
+ */
28
+ function printHuman(result) {
29
+ const label =
30
+ result.version != null
31
+ ? `${result.name}@${result.version}`
32
+ : result.name;
33
+ humanLog(`Validating integration: ${label}`);
34
+
35
+ if (result.issues.length === 0) {
36
+ humanLog('\n\u2713 No issues found.');
37
+ return;
38
+ }
39
+
40
+ humanLog('');
41
+ for (const issue of result.issues) {
42
+ humanLog(` ${issue.severity} ${issue.code}: ${issue.message}`);
43
+ }
44
+
45
+ const {errors, warnings} = summarizeIssues(result.issues);
46
+ humanLog(
47
+ `\n${result.issues.length} issue(s): ${errors} error(s), ${warnings} warning(s)`,
48
+ );
49
+ }
50
+
51
+ const NO_MANIFEST_GUIDANCE =
52
+ 'No astryx.integration.* found next to package.json. ' +
53
+ 'To validate an installed integration: astryx validate-integration <package>';
54
+
55
+ /**
56
+ * Register the `astryx validate-integration` command.
57
+ * @param {import('commander').Command} program
58
+ */
59
+ export function registerValidateIntegration(program) {
60
+ program
61
+ .command('validate-integration [package]')
62
+ .description(
63
+ 'Validate an Astryx integration package (manifest + contributions)',
64
+ )
65
+ .addHelpText(
66
+ 'after',
67
+ '\nWith no argument, validates the integration package rooted at the\n' +
68
+ 'current directory. Pass a package name to validate an installed\n' +
69
+ 'integration resolved from ./node_modules.\n\n' +
70
+ 'Exit code:\n' +
71
+ ' 0 no error issues (warnings are allowed) — safe as a CI gate\n' +
72
+ ' 1 one or more error issues\n',
73
+ )
74
+ .action(async pkg => {
75
+ const json = program.opts().json || false;
76
+
77
+ const result = pkg
78
+ ? await validateInstalledIntegration(pkg)
79
+ : await validateLocalIntegration();
80
+
81
+ // No-arg + no local manifest: guidance, not an error.
82
+ if (!result.found) {
83
+ if (json) {
84
+ jsonOut('integration.validate', {
85
+ name: null,
86
+ version: null,
87
+ issues: [],
88
+ });
89
+ } else {
90
+ humanLog(NO_MANIFEST_GUIDANCE);
91
+ }
92
+ return;
93
+ }
94
+
95
+ if (json) {
96
+ jsonOut('integration.validate', {
97
+ name: result.name ?? null,
98
+ version: result.version ?? null,
99
+ issues: result.issues,
100
+ });
101
+ } else {
102
+ printHuman(result);
103
+ }
104
+
105
+ const {errors} = summarizeIssues(result.issues);
106
+ if (errors > 0) {
107
+ process.exitCode = 1;
108
+ }
109
+ });
110
+ }
@@ -0,0 +1,124 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Command-level tests for `astryx validate-integration` — envelope shape
5
+ * and the no-manifest guidance path.
6
+ */
7
+
8
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import {Command} from 'commander';
12
+
13
+ import {registerValidateIntegration} from './validate-integration.mjs';
14
+
15
+ let tmpDir;
16
+ let logCalls;
17
+ let prevCwd;
18
+ let prevExit;
19
+
20
+ beforeEach(() => {
21
+ // Temp dir under the repo root so manifest/node_modules paths are within
22
+ // Vite's allowed fs roots.
23
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-validate-cmd-'));
24
+ logCalls = [];
25
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
26
+ logCalls.push(args.join(' '));
27
+ });
28
+ vi.spyOn(console, 'error').mockImplementation(() => {});
29
+ prevCwd = process.cwd();
30
+ prevExit = process.exitCode;
31
+ process.exitCode = undefined;
32
+ });
33
+
34
+ afterEach(() => {
35
+ process.chdir(prevCwd);
36
+ process.exitCode = prevExit;
37
+ fs.rmSync(tmpDir, {recursive: true, force: true});
38
+ vi.restoreAllMocks();
39
+ });
40
+
41
+ function createProgram() {
42
+ const program = new Command();
43
+ program.exitOverride();
44
+ program.option('--json', 'Output as typed JSON');
45
+ registerValidateIntegration(program);
46
+ return program;
47
+ }
48
+
49
+ describe('validate-integration — command', () => {
50
+ it('--json emits an integration.validate envelope for a valid package', async () => {
51
+ fs.writeFileSync(
52
+ path.join(tmpDir, 'package.json'),
53
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
54
+ );
55
+ fs.writeFileSync(
56
+ path.join(tmpDir, 'astryx.integration.mjs'),
57
+ 'export default {};\n',
58
+ );
59
+ process.chdir(tmpDir);
60
+
61
+ const program = createProgram();
62
+ await program.parseAsync(['node', 'astryx', '--json', 'validate-integration']);
63
+
64
+ const parsed = JSON.parse(logCalls.join('\n'));
65
+ expect(parsed.apiVersion).toBe(1);
66
+ expect(parsed.type).toBe('integration.validate');
67
+ expect(parsed.data.name).toBe('@acme/widgets');
68
+ expect(parsed.data.version).toBe('1.2.3');
69
+ expect(Array.isArray(parsed.data.issues)).toBe(true);
70
+ expect(parsed.data.issues).toHaveLength(0);
71
+ expect(process.exitCode).toBeUndefined();
72
+ });
73
+
74
+ it('--json envelope carries issues and sets exit 1 on errors', async () => {
75
+ fs.writeFileSync(
76
+ path.join(tmpDir, 'package.json'),
77
+ JSON.stringify({name: '@acme/bad', version: '1.0.0'}),
78
+ );
79
+ fs.writeFileSync(
80
+ path.join(tmpDir, 'astryx.integration.mjs'),
81
+ 'export default { templates: "./gone" };\n',
82
+ );
83
+ process.chdir(tmpDir);
84
+
85
+ const program = createProgram();
86
+ await program.parseAsync(['node', 'astryx', '--json', 'validate-integration']);
87
+
88
+ const parsed = JSON.parse(logCalls.join('\n'));
89
+ expect(parsed.type).toBe('integration.validate');
90
+ expect(parsed.data.issues.some(i => i.code === 'missing_root')).toBe(true);
91
+ expect(process.exitCode).toBe(1);
92
+ });
93
+
94
+ it('no-arg + no manifest prints guidance and stays exit 0', async () => {
95
+ fs.writeFileSync(
96
+ path.join(tmpDir, 'package.json'),
97
+ JSON.stringify({name: 'plain'}),
98
+ );
99
+ process.chdir(tmpDir);
100
+
101
+ const program = createProgram();
102
+ await program.parseAsync(['node', 'astryx', 'validate-integration']);
103
+
104
+ const out = logCalls.join('\n');
105
+ expect(out).toContain('No astryx.integration.* found next to package.json');
106
+ expect(process.exitCode).toBeUndefined();
107
+ });
108
+
109
+ it('--json with no manifest yields a null-identity envelope', async () => {
110
+ fs.writeFileSync(
111
+ path.join(tmpDir, 'package.json'),
112
+ JSON.stringify({name: 'plain'}),
113
+ );
114
+ process.chdir(tmpDir);
115
+
116
+ const program = createProgram();
117
+ await program.parseAsync(['node', 'astryx', '--json', 'validate-integration']);
118
+
119
+ const parsed = JSON.parse(logCalls.join('\n'));
120
+ expect(parsed.type).toBe('integration.validate');
121
+ expect(parsed.data.name).toBeNull();
122
+ expect(parsed.data.issues).toEqual([]);
123
+ });
124
+ });
package/src/config.mjs CHANGED
@@ -1,31 +1,18 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- import {validateConfig, validateIntegration} from './lib/config-schema.mjs';
4
-
5
3
  /**
6
- * Type-preserving helpers for Astryx config and integration manifests.
4
+ * Type-preserving helper for the Astryx config file.
5
+ *
6
+ * This is an intentionally tiny runtime identity function: it returns its
7
+ * argument unchanged. Its value is the exported TypeScript surface from
8
+ * `@astryxdesign/cli/config`, so config files get editor/type feedback without
9
+ * coupling to CLI internals. Validation is NOT performed here — it happens at
10
+ * the load boundary (see `loadModuleWithSchema` + `AstryxConfigSchema`).
7
11
  *
8
- * These are intentionally tiny runtime identity functions. Their value is the
9
- * exported TypeScript surface from `@astryxdesign/cli/config`, so config files
10
- * can get editor/type feedback without coupling to CLI internals.
11
- */
12
-
13
- /**
14
12
  * @template {import('./types/config').AstryxConfig} T
15
13
  * @param {T} config
16
14
  * @returns {T}
17
15
  */
18
16
  export function createConfig(config) {
19
- validateConfig(config);
20
17
  return config;
21
18
  }
22
-
23
- /**
24
- * @template {import('./types/config').AstryxIntegration} T
25
- * @param {T} integration
26
- * @returns {T}
27
- */
28
- export function createIntegration(integration) {
29
- validateIntegration(integration);
30
- return integration;
31
- }
@@ -1,24 +1,100 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  import {describe, expect, it} from 'vitest';
4
- import {createConfig, createIntegration} from './config.mjs';
4
+ import {createConfig} from './config.mjs';
5
+ import {createIntegration} from './integration.mjs';
6
+ import {
7
+ AstryxConfigSchema,
8
+ AstryxIntegrationSchema,
9
+ } from './lib/config-schema.mjs';
5
10
 
6
- describe('config helpers', () => {
7
- it('return config and integration objects unchanged', () => {
11
+ // createConfig/createIntegration are now pure typed-identity helpers: they
12
+ // return their argument unchanged and perform NO runtime validation. Validation
13
+ // happens at the LOAD boundary (loadModuleWithSchema against the schemas below),
14
+ // so the rejection cases that used to assert factory throws now assert the
15
+ // schema rejects the same shapes.
16
+
17
+ describe('createConfig (typed identity)', () => {
18
+ it('returns the config unchanged', () => {
8
19
  const config = {integrations: ['@acme/widgets']};
9
- const integration = {name: '@acme/widgets', docs: './docs'};
10
20
  expect(createConfig(config)).toBe(config);
11
- expect(createIntegration(integration)).toBe(integration);
21
+ expect(createConfig({})).toEqual({});
22
+ });
23
+
24
+ it('does NOT validate — returns invalid shapes unchanged', () => {
25
+ const bogus = {packages: ['./libs']};
26
+ expect(createConfig(bogus)).toBe(bogus);
27
+ const badIntegrations = {integrations: '@acme/widgets'};
28
+ expect(createConfig(badIntegrations)).toBe(badIntegrations);
29
+ });
30
+ });
31
+
32
+ describe('AstryxConfigSchema (load-boundary validation)', () => {
33
+ it('accepts a valid minimal config', () => {
34
+ expect(AstryxConfigSchema.parse({})).toEqual({});
35
+ expect(
36
+ AstryxConfigSchema.parse({integrations: ['@acme/widgets']}),
37
+ ).toEqual({integrations: ['@acme/widgets']});
12
38
  });
13
39
 
14
- it('validates config and integration shapes', () => {
15
- expect(() => createConfig({integrations: [42]})).toThrow(/integrations/);
16
- expect(() => createIntegration({docs: './docs'})).toThrow(/name/);
40
+ it('accepts hooks.postCodemod with a buildCommand function', () => {
41
+ const config = {
42
+ hooks: {postCodemod: [{name: 'format', buildCommand: () => null}]},
43
+ };
44
+ expect(() => AstryxConfigSchema.parse(config)).not.toThrow();
45
+ });
46
+
47
+ it('rejects unknown keys (strict)', () => {
48
+ expect(() => AstryxConfigSchema.parse({packages: ['./libs']})).toThrow(
49
+ /packages|Unrecognized/,
50
+ );
51
+ });
52
+
53
+ it('rejects a non-array integrations field', () => {
17
54
  expect(() =>
18
- createIntegration({
19
- name: '@acme/widgets',
20
- postCodemod: [{name: 'empty'}],
21
- }),
22
- ).toThrow(/postCodemod/);
55
+ AstryxConfigSchema.parse({integrations: '@acme/widgets'}),
56
+ ).toThrow();
57
+ });
58
+
59
+ it('rejects a non-URL issuesUrl', () => {
60
+ expect(() => AstryxConfigSchema.parse({issuesUrl: 'not-a-url'})).toThrow();
61
+ });
62
+
63
+ it('rejects a postCodemod hook without buildCommand', () => {
64
+ expect(() =>
65
+ AstryxConfigSchema.parse({hooks: {postCodemod: [{name: 'empty'}]}}),
66
+ ).toThrow();
67
+ });
68
+ });
69
+
70
+ describe('createIntegration (typed identity)', () => {
71
+ it('returns the integration unchanged', () => {
72
+ const integration = {components: './src'};
73
+ expect(createIntegration(integration)).toBe(integration);
74
+ expect(createIntegration({})).toEqual({});
75
+ });
76
+
77
+ it('does NOT validate — returns invalid shapes unchanged', () => {
78
+ const bogus = {name: '@acme/widgets'};
79
+ expect(createIntegration(bogus)).toBe(bogus);
80
+ });
81
+ });
82
+
83
+ describe('AstryxIntegrationSchema (load-boundary validation)', () => {
84
+ it('accepts a valid minimal integration', () => {
85
+ expect(AstryxIntegrationSchema.parse({components: './src'})).toEqual({
86
+ components: './src',
87
+ });
88
+ expect(AstryxIntegrationSchema.parse({})).toEqual({});
89
+ });
90
+
91
+ it('rejects unknown keys (strict)', () => {
92
+ expect(() => AstryxIntegrationSchema.parse({name: '@acme/widgets'})).toThrow(
93
+ /name|Unrecognized/,
94
+ );
95
+ });
96
+
97
+ it('rejects a non-URL issuesUrl', () => {
98
+ expect(() => AstryxIntegrationSchema.parse({issuesUrl: 'nope'})).toThrow();
23
99
  });
24
100
  });
package/src/index.mjs CHANGED
@@ -61,10 +61,10 @@ export const JSON_SUPPORTED = new Set([
61
61
  'theme build',
62
62
  'theme list',
63
63
  'theme add',
64
- 'gap-report',
65
64
  'upgrade',
66
65
  'manifest',
67
66
  'doctor',
67
+ 'validate-integration',
68
68
  'layout expand',
69
69
  'layout check',
70
70
  'layout grammar',
@@ -155,7 +155,7 @@ program
155
155
  });
156
156
 
157
157
  /**
158
- * Compute the fully qualified command name, e.g. "theme build" or "gap-report".
158
+ * Compute the fully qualified command name, e.g. "theme build" or "swizzle".
159
159
  * @param {import('commander').Command} actionCommand
160
160
  * @returns {string}
161
161
  */
@@ -245,11 +245,11 @@ const commands = [
245
245
  {name: 'init', path: './commands/init.mjs', register: 'registerInit'},
246
246
  {name: 'component', path: './commands/component/index.mjs', register: 'registerComponent'},
247
247
  {name: 'docs', path: './commands/docs.mjs', register: 'registerDocs'},
248
+ {name: 'blog', path: './commands/blog.mjs', register: 'registerBlog'},
248
249
  {name: 'swizzle', path: './commands/swizzle.mjs', register: 'registerSwizzle'},
249
250
  // agent-docs folded into init — functions still importable from agent-docs.mjs
250
251
  {name: 'template', path: './commands/template.mjs', register: 'registerTemplate'},
251
252
  {name: 'layout', path: './commands/layout.mjs', register: 'registerLayout'},
252
- {name: 'gap-report', path: './commands/gap-report.mjs', register: 'registerGapReport'},
253
253
  {name: 'upgrade', path: './commands/upgrade.mjs', register: 'registerUpgrade'},
254
254
  {name: 'theme', path: './commands/build-theme.mjs', register: 'registerTheme'},
255
255
  {name: 'hook', path: './commands/hook/index.mjs', register: 'registerHook'},
@@ -257,6 +257,11 @@ const commands = [
257
257
  {name: 'search', path: './commands/search.mjs', register: 'registerSearch'},
258
258
  {name: 'build', path: './commands/build.mjs', register: 'registerBuild'},
259
259
  {name: 'doctor', path: './commands/doctor.mjs', register: 'registerDoctor'},
260
+ {
261
+ name: 'validate-integration',
262
+ path: './commands/validate-integration.mjs',
263
+ register: 'registerValidateIntegration',
264
+ },
260
265
  ];
261
266
 
262
267
  for (const cmd of commands) {
@@ -0,0 +1,19 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Type-preserving helper for an Astryx integration manifest.
5
+ *
6
+ * This is an intentionally tiny runtime identity function: it returns its
7
+ * argument unchanged. Its value is the exported TypeScript surface from
8
+ * `@astryxdesign/cli/integration`, so manifests get editor/type feedback
9
+ * without coupling to CLI internals. Validation is NOT performed here — it
10
+ * happens at the load boundary (see `loadModuleWithSchema` +
11
+ * `AstryxIntegrationSchema`).
12
+ *
13
+ * @template {import('./types/integration').AstryxIntegration} T
14
+ * @param {T} integration
15
+ * @returns {T}
16
+ */
17
+ export function createIntegration(integration) {
18
+ return integration;
19
+ }
@@ -9,6 +9,12 @@ import * as path from 'node:path';
9
9
 
10
10
  const SKIP_DIRS = new Set(['hooks', 'utils', '__tests__', 'node_modules']);
11
11
 
12
+ /** The owner package name for built-in (core) components. */
13
+ export const CORE_PACKAGE = '@astryxdesign/core';
14
+
15
+ /** Conventional doc-file suffixes for integration components (same-stem). */
16
+ const INTEGRATION_DOC_SUFFIXES = ['.doc.ts', '.doc.mjs', '.doc.js'];
17
+
12
18
  // Component source files are named `XDS{Name}.tsx` today. The XDS-prefix
13
19
  // migration (P2380608025, P4) renames them to the bare `{Name}.tsx` form, so
14
20
  // discovery must recognize BOTH. The `XDS` prefix has historically doubled as
@@ -475,6 +481,177 @@ export function findExternalComponentDoc(docsDir, name) {
475
481
  return scanDir(docsDir);
476
482
  }
477
483
 
484
+ // ── Integration component discovery (ownership-aware) ────────────────
485
+ //
486
+ // Integration packages contribute a `components` root (resolved absolute path
487
+ // in `loadedIntegrations`, see lib/integrations.mjs). Each component uses a
488
+ // same-stem source/doc convention — e.g. `MetaAppShell.tsx` next to
489
+ // `MetaAppShell.doc.{ts,mjs,js}`. The doc file is authoritative for discovery;
490
+ // the sibling `.tsx` (if present) is the swizzleable source.
491
+ //
492
+ // Each discovered component is recorded with its OWNER package (the
493
+ // integration's package name) and the owner's `issuesUrl` so downstream
494
+ // commands — and the future integration-component swizzle — can disambiguate
495
+ // by package and route source/issues correctly.
496
+
497
+ /**
498
+ * Given an integration component doc path, return the sibling component source
499
+ * (`{Name}.tsx`) if one exists, else null.
500
+ *
501
+ * @param {string} docPath absolute path to a `{Name}.doc.{ts,mjs,js}` file
502
+ * @returns {string|null}
503
+ */
504
+ function integrationSourceForDoc(docPath) {
505
+ const dir = path.dirname(docPath);
506
+ const base = path.basename(docPath).replace(/\.doc\.(ts|mjs|js)$/, '');
507
+ const candidate = path.join(dir, `${base}.tsx`);
508
+ return fs.existsSync(candidate) ? candidate : null;
509
+ }
510
+
511
+ /**
512
+ * Discover ownership records for the components contributed by a single loaded
513
+ * integration. Scans the integration's resolved `components` dir for same-stem
514
+ * doc files and records each with owner package + issuesUrl + sourcePath.
515
+ *
516
+ * @param {{name: string, components?: string, issuesUrl?: string}} integration
517
+ * a single entry from `loadedIntegrations` (lib/integrations.mjs)
518
+ * @returns {Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>}
519
+ */
520
+ export function discoverIntegrationComponents(integration) {
521
+ const componentsDir = integration?.components;
522
+ if (!componentsDir || !fs.existsSync(componentsDir)) return [];
523
+
524
+ /** @type {Map<string, {name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>} */
525
+ const byName = new Map();
526
+
527
+ function scanDir(dirPath) {
528
+ const entries = fs.readdirSync(dirPath, {withFileTypes: true});
529
+ for (const entry of entries) {
530
+ if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
531
+ const fullPath = path.join(dirPath, entry.name);
532
+ if (entry.isDirectory()) {
533
+ scanDir(fullPath);
534
+ continue;
535
+ }
536
+ const suffix = INTEGRATION_DOC_SUFFIXES.find(s => entry.name.endsWith(s));
537
+ if (!suffix) continue;
538
+ const name = entry.name.slice(0, -suffix.length);
539
+ const {group, hidden} = readDocMeta(fullPath);
540
+ if (hidden) continue;
541
+ // First doc wins per name (precedence matches INTEGRATION_DOC_SUFFIXES).
542
+ if (byName.has(name)) continue;
543
+ byName.set(name, {
544
+ name,
545
+ package: integration.name,
546
+ docPath: fullPath,
547
+ sourcePath: integrationSourceForDoc(fullPath),
548
+ issuesUrl: integration.issuesUrl,
549
+ group: group ?? null,
550
+ });
551
+ }
552
+ }
553
+
554
+ scanDir(componentsDir);
555
+ return [...byName.values()];
556
+ }
557
+
558
+ /**
559
+ * Find an integration component's doc file by name within a loaded
560
+ * integration's resolved `components` dir. Honors the same-stem convention
561
+ * (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js`.
562
+ *
563
+ * @param {{components?: string}} integration
564
+ * @param {string} name bare component name (no `XDS`/`Astryx` prefix)
565
+ * @returns {string|null}
566
+ */
567
+ export function findIntegrationComponentDoc(integration, name) {
568
+ const componentsDir = integration?.components;
569
+ if (!componentsDir || !fs.existsSync(componentsDir)) return null;
570
+
571
+ function scanDir(dirPath) {
572
+ const entries = fs.readdirSync(dirPath, {withFileTypes: true});
573
+ // Exact same-stem match (precedence order) first in this dir.
574
+ for (const suffix of INTEGRATION_DOC_SUFFIXES) {
575
+ const candidate = path.join(dirPath, `${name}${suffix}`);
576
+ if (fs.existsSync(candidate)) return candidate;
577
+ }
578
+ for (const entry of entries) {
579
+ if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
580
+ if (entry.isDirectory()) {
581
+ const found = scanDir(path.join(dirPath, entry.name));
582
+ if (found) return found;
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+
588
+ return scanDir(componentsDir);
589
+ }
590
+
591
+ /**
592
+ * Find an integration component's swizzleable source file (`{Name}.tsx`) by
593
+ * name within a loaded integration's resolved `components` dir. Returns null
594
+ * when the integration ships docs without source.
595
+ *
596
+ * @param {{components?: string}} integration
597
+ * @param {string} name bare component name
598
+ * @returns {string|null}
599
+ */
600
+ export function findIntegrationComponentSource(integration, name) {
601
+ const docPath = findIntegrationComponentDoc(integration, name);
602
+ if (!docPath) return null;
603
+ return integrationSourceForDoc(docPath);
604
+ }
605
+
606
+ /**
607
+ * Build a flat list of ownership records for ALL discoverable components —
608
+ * core (built-in) plus every loaded integration. This is the authoritative
609
+ * source for package-aware listing and disambiguation.
610
+ *
611
+ * Core records carry `package: '@astryxdesign/core'`, `issuesUrl: undefined`
612
+ * (the default core issues URL), and the resolved `.tsx` source via
613
+ * findComponentSource(). Integration records carry their owner package name,
614
+ * the manifest `issuesUrl`, and the same-stem `.tsx` source if present.
615
+ *
616
+ * @param {string} coreDir
617
+ * @param {Array<{name: string, components?: string, issuesUrl?: string}>} [loadedIntegrations]
618
+ * @returns {Array<{name: string, package: string, group: string|null, docPath: string|null, sourcePath: string|null, issuesUrl: string|undefined}>}
619
+ */
620
+ export function discoverOwnedComponents(coreDir, loadedIntegrations = []) {
621
+ /** @type {Array<{name: string, package: string, group: string|null, docPath: string|null, sourcePath: string|null, issuesUrl: string|undefined}>} */
622
+ const records = [];
623
+
624
+ // Core components — derive group from discoverComponents (grouped record).
625
+ const grouped = discoverComponents(coreDir);
626
+ /** @type {Map<string, string|null>} name → group */
627
+ const coreGroup = new Map();
628
+ for (const [key, members] of Object.entries(grouped)) {
629
+ const isUngrouped = members.length === 1 && members[0] === key;
630
+ for (const name of members) {
631
+ coreGroup.set(name, isUngrouped ? null : key);
632
+ }
633
+ }
634
+ for (const [name, group] of coreGroup) {
635
+ records.push({
636
+ name,
637
+ package: CORE_PACKAGE,
638
+ group,
639
+ docPath: findComponentReadme(coreDir, name),
640
+ sourcePath: findComponentSource(coreDir, name),
641
+ issuesUrl: undefined,
642
+ });
643
+ }
644
+
645
+ // Integration components.
646
+ for (const integration of loadedIntegrations) {
647
+ for (const rec of discoverIntegrationComponents(integration)) {
648
+ records.push(rec);
649
+ }
650
+ }
651
+
652
+ return records;
653
+ }
654
+
478
655
  // ── Legacy markdown-parsing functions ────────────────────────────────
479
656
  // These are kept for backward compatibility with existing tests.
480
657
  // The CLI action handler uses the new format functions below instead.