@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,227 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import * as os from 'node:os';
7
+ import {
8
+ discoverIntegrationComponents,
9
+ discoverOwnedComponents,
10
+ findIntegrationComponentDoc,
11
+ findIntegrationComponentSource,
12
+ CORE_PACKAGE,
13
+ } from '../lib/component-discovery.mjs';
14
+
15
+ // The api `component()` reads integrations via Project.load(). In the vitest
16
+ // environment, Vite's `server.fs.allow` only permits loading modules from
17
+ // under `node_modules`, so a real astryx.config.mjs at a tmp root cannot be
18
+ // imported. We therefore mock Project.load to return a project whose
19
+ // `loadedIntegrations` are resolved (exactly the shape lib/integrations.mjs
20
+ // produces) while keeping the integration's `components` dir on disk under
21
+ // node_modules so its `.doc.mjs` files load normally.
22
+ const projectLoadMock = vi.fn();
23
+ vi.mock('../lib/project.mjs', () => ({
24
+ Project: {load: (...args) => projectLoadMock(...args)},
25
+ }));
26
+
27
+ // Import the api AFTER the mock is registered.
28
+ const {component} = await import('../api/component.mjs');
29
+
30
+ let tmpDir;
31
+
32
+ const INTEGRATION_NAME = '@test/meta';
33
+ const INTEGRATION_ISSUES = 'https://example.com/meta/issues';
34
+
35
+ /**
36
+ * Build a consumer fixture:
37
+ * - packages/core symlinked to the real @astryxdesign/core (loadDocs source)
38
+ * - node_modules/@test/meta with a `components` dir using the same-stem
39
+ * source/doc convention (MetaAppShell.tsx + MetaAppShell.doc.mjs)
40
+ * Returns the absolute `components` dir so the Project.load mock can hand back a
41
+ * resolved integration entry.
42
+ */
43
+ function createFixture({withSource = true, extraComponent = null} = {}) {
44
+ const realCoreDir = path.resolve(import.meta.dirname, '..', '..', '..', 'core');
45
+ const coreDir = path.join(tmpDir, 'packages', 'core');
46
+ fs.mkdirSync(path.dirname(coreDir), {recursive: true});
47
+ fs.symlinkSync(realCoreDir, coreDir);
48
+
49
+ const intDir = path.join(tmpDir, 'node_modules', '@test', 'meta');
50
+ const compDir = path.join(intDir, 'components');
51
+ fs.mkdirSync(compDir, {recursive: true});
52
+ fs.writeFileSync(
53
+ path.join(intDir, 'package.json'),
54
+ JSON.stringify({name: INTEGRATION_NAME, version: '1.2.3'}),
55
+ );
56
+ fs.writeFileSync(
57
+ path.join(compDir, 'MetaAppShell.doc.mjs'),
58
+ `export const docs = {\n name: 'MetaAppShell',\n usage: { description: 'Meta-flavored app shell.' },\n props: [{ name: 'title', type: 'string', description: 'Header title' }],\n};\n`,
59
+ );
60
+ if (withSource) {
61
+ fs.writeFileSync(
62
+ path.join(compDir, 'MetaAppShell.tsx'),
63
+ "'use client';\nexport function MetaAppShell() { return null; }\n",
64
+ );
65
+ }
66
+ if (extraComponent) {
67
+ fs.writeFileSync(
68
+ path.join(compDir, `${extraComponent}.doc.mjs`),
69
+ `export const docs = {\n name: '${extraComponent}',\n usage: { description: '${extraComponent} from meta.' },\n};\n`,
70
+ );
71
+ fs.writeFileSync(
72
+ path.join(compDir, `${extraComponent}.tsx`),
73
+ `export function ${extraComponent}() { return null; }\n`,
74
+ );
75
+ }
76
+
77
+ const integration = {
78
+ name: INTEGRATION_NAME,
79
+ version: '1.2.3',
80
+ components: compDir,
81
+ templates: undefined,
82
+ codemods: undefined,
83
+ issuesUrl: INTEGRATION_ISSUES,
84
+ };
85
+ projectLoadMock.mockResolvedValue({
86
+ integrations: [INTEGRATION_NAME],
87
+ loadedIntegrations: [integration],
88
+ });
89
+ return {coreDir, intDir, compDir, integration};
90
+ }
91
+
92
+ beforeEach(() => {
93
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-ownership-'));
94
+ projectLoadMock.mockReset();
95
+ // Default: no integrations (core only).
96
+ projectLoadMock.mockResolvedValue({integrations: [], loadedIntegrations: []});
97
+ });
98
+
99
+ afterEach(() => {
100
+ fs.rmSync(tmpDir, {recursive: true, force: true});
101
+ });
102
+
103
+ describe('discoverIntegrationComponents (ownership records)', () => {
104
+ it('records name, package, sourcePath, and issuesUrl for same-stem components', () => {
105
+ const {integration, compDir} = createFixture();
106
+ const records = discoverIntegrationComponents(integration);
107
+ expect(records).toHaveLength(1);
108
+ const rec = records[0];
109
+ expect(rec.name).toBe('MetaAppShell');
110
+ expect(rec.package).toBe(INTEGRATION_NAME);
111
+ expect(rec.issuesUrl).toBe(INTEGRATION_ISSUES);
112
+ expect(rec.sourcePath).toBe(path.join(compDir, 'MetaAppShell.tsx'));
113
+ expect(fs.existsSync(rec.sourcePath)).toBe(true);
114
+ });
115
+
116
+ it('records sourcePath: null when the integration ships docs without source', () => {
117
+ const {integration} = createFixture({withSource: false});
118
+ const [rec] = discoverIntegrationComponents(integration);
119
+ expect(rec.sourcePath).toBeNull();
120
+ });
121
+ });
122
+
123
+ describe('discoverOwnedComponents (core + integrations)', () => {
124
+ it('marks core components with the core package and integration components with their owner', () => {
125
+ const {coreDir, integration} = createFixture();
126
+ const records = discoverOwnedComponents(coreDir, [integration]);
127
+ const core = records.find(r => r.package === CORE_PACKAGE);
128
+ expect(core).toBeTruthy();
129
+ expect(core.issuesUrl).toBeUndefined();
130
+ const meta = records.find(r => r.name === 'MetaAppShell');
131
+ expect(meta.package).toBe(INTEGRATION_NAME);
132
+ expect(meta.issuesUrl).toBe(INTEGRATION_ISSUES);
133
+ expect(meta.sourcePath).toContain('MetaAppShell.tsx');
134
+ });
135
+ });
136
+
137
+ describe('findIntegrationComponentDoc / Source', () => {
138
+ it('finds doc + source by name', () => {
139
+ const {integration} = createFixture();
140
+ expect(findIntegrationComponentDoc(integration, 'MetaAppShell')).toContain('MetaAppShell.doc.mjs');
141
+ expect(findIntegrationComponentSource(integration, 'MetaAppShell')).toContain('MetaAppShell.tsx');
142
+ });
143
+
144
+ it('returns null source when none present', () => {
145
+ const {integration} = createFixture({withSource: false});
146
+ expect(findIntegrationComponentSource(integration, 'MetaAppShell')).toBeNull();
147
+ });
148
+ });
149
+
150
+ describe('component() — integration ownership via config', () => {
151
+ it('discovers a config integration component by package ownership (detail)', async () => {
152
+ createFixture();
153
+ const result = await component('MetaAppShell', {cwd: tmpDir});
154
+ expect(result.type).toBe('component.detail');
155
+ expect(result.data.name).toBe('MetaAppShell');
156
+ expect(result.data.package).toBe(INTEGRATION_NAME);
157
+ expect(result.data.sourceAvailable).toBe(true);
158
+ expect(result.data.import).toBe(`${INTEGRATION_NAME}/MetaAppShell`);
159
+ });
160
+
161
+ it('--package resolves the integration component', async () => {
162
+ createFixture();
163
+ const result = await component('MetaAppShell', {cwd: tmpDir, package: INTEGRATION_NAME});
164
+ expect(result.type).toBe('component.detail');
165
+ expect(result.data.package).toBe(INTEGRATION_NAME);
166
+ });
167
+
168
+ it('--source returns the integration source when available', async () => {
169
+ createFixture();
170
+ const result = await component('MetaAppShell', {cwd: tmpDir, source: true});
171
+ expect(result.type).toBe('component.detail.source');
172
+ expect(result.data.component).toBe('MetaAppShell');
173
+ expect(result.data.source).toContain('MetaAppShell');
174
+ });
175
+
176
+ it('--source throws ERR_NO_SOURCE when the integration ships no source', async () => {
177
+ createFixture({withSource: false});
178
+ await expect(
179
+ component('MetaAppShell', {cwd: tmpDir, source: true}),
180
+ ).rejects.toMatchObject({code: 'ERR_NO_SOURCE'});
181
+ });
182
+
183
+ it('errors with candidate packages when a name is ambiguous (core + integration)', async () => {
184
+ // 'AppShell' exists in core; add a same-named integration component.
185
+ createFixture({extraComponent: 'AppShell'});
186
+ let caught;
187
+ try {
188
+ await component('AppShell', {cwd: tmpDir});
189
+ } catch (e) {
190
+ caught = e;
191
+ }
192
+ expect(caught).toBeTruthy();
193
+ expect(caught.code).toBe('ERR_UNKNOWN_COMPONENT');
194
+ const pkgs = (caught.suggestions ?? []).map(s => s.name);
195
+ expect(pkgs).toContain(CORE_PACKAGE);
196
+ expect(pkgs).toContain(INTEGRATION_NAME);
197
+ });
198
+
199
+ it('--package disambiguates an ambiguous name to core', async () => {
200
+ createFixture({extraComponent: 'AppShell'});
201
+ const result = await component('AppShell', {cwd: tmpDir, package: CORE_PACKAGE});
202
+ expect(result.type).toBe('component.detail');
203
+ expect(result.data.package).toBe(CORE_PACKAGE);
204
+ });
205
+
206
+ it('--package disambiguates an ambiguous name to the integration', async () => {
207
+ createFixture({extraComponent: 'AppShell'});
208
+ const result = await component('AppShell', {cwd: tmpDir, package: INTEGRATION_NAME});
209
+ expect(result.type).toBe('component.detail');
210
+ expect(result.data.package).toBe(INTEGRATION_NAME);
211
+ });
212
+
213
+ it('JSON list includes integration components as {name, package} objects', async () => {
214
+ createFixture();
215
+ const result = await component(undefined, {cwd: tmpDir, list: true});
216
+ expect(result.type).toBe('component.list');
217
+ const allEntries = Object.values(result.data).flat();
218
+ for (const entry of allEntries) {
219
+ expect(typeof entry.name).toBe('string');
220
+ expect(typeof entry.package).toBe('string');
221
+ }
222
+ const meta = allEntries.find(e => e.name === 'MetaAppShell');
223
+ expect(meta).toBeTruthy();
224
+ expect(meta.package).toBe(INTEGRATION_NAME);
225
+ expect(allEntries.some(e => e.package === CORE_PACKAGE)).toBe(true);
226
+ });
227
+ });
@@ -10,7 +10,6 @@
10
10
  * astryx discover searchterm Search across all packages
11
11
  */
12
12
 
13
- import {loadConfig} from '../lib/config.mjs';
14
13
  import {formatFull, formatBrief, formatCompact} from '../lib/component-format.mjs';
15
14
  import {jsonOut, humanLog} from '../lib/json.mjs';
16
15
  import {cliError} from '../lib/cli-error.mjs';
@@ -45,27 +44,17 @@ export function registerDiscover(program) {
45
44
  switch (result.type) {
46
45
  case 'discover.list': {
47
46
  if (result.data.length === 0) {
48
- const config = await loadConfig();
49
47
  humanLog('');
50
- if (config.packages.length === 0) {
51
- humanLog('No package directories configured.');
48
+ if (result.meta && result.meta.configured === false) {
49
+ humanLog('No integrations configured.');
52
50
  humanLog('');
53
- humanLog('Add a packages field to astryx.config.mjs:');
51
+ humanLog('Add integration package names to astryx.config.mjs:');
54
52
  humanLog('');
55
53
  humanLog(' export default {');
56
- humanLog(" packages: ['/path/to/your/libs'],");
54
+ humanLog(" integrations: ['@scope/your-integration'],");
57
55
  humanLog(' };');
58
56
  } else {
59
- humanLog('No external packages found.');
60
- humanLog('');
61
- humanLog('Packages opt in by adding an "astryx" field to package.json:');
62
- humanLog('');
63
- humanLog(' {');
64
- humanLog(' "astryx": {');
65
- humanLog(' "docs": "./src",');
66
- humanLog(' "category": "Common"');
67
- humanLog(' }');
68
- humanLog(' }');
57
+ humanLog('No external components found in configured integrations.');
69
58
  }
70
59
  humanLog('');
71
60
  } else {
@@ -132,7 +132,7 @@ describe('doctor — individual checks', () => {
132
132
  );
133
133
  const configPath = path.join(fixtureDir, 'astryx.config.mjs');
134
134
  try {
135
- fs.writeFileSync(configPath, 'export default { theme: "default" };');
135
+ fs.writeFileSync(configPath, 'export default { integrations: [] };');
136
136
  const res = await checkConfig({cwd: fixtureDir, configPath});
137
137
  if (res.status !== 'pass') {
138
138
  throw new Error(`expected pass, got ${res.status}: ${res.message}`);
@@ -158,13 +158,13 @@ describe('doctor — individual checks', () => {
158
158
  }
159
159
  });
160
160
 
161
- it('config: FAIL when packages is the wrong shape', async () => {
161
+ it('config: FAIL when default export is not an object', async () => {
162
162
  const fixtureDir = fs.mkdtempSync(
163
163
  path.join(path.dirname(new URL(import.meta.url).pathname), '__doctor_cfg_'),
164
164
  );
165
165
  const configPath = path.join(fixtureDir, 'astryx.config.mjs');
166
166
  try {
167
- fs.writeFileSync(configPath, 'export default { packages: [123] };');
167
+ fs.writeFileSync(configPath, 'export default 123;');
168
168
  const res = await checkConfig({cwd: fixtureDir, configPath});
169
169
  expect(res.status).toBe('fail');
170
170
  } finally {
@@ -0,0 +1,120 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Shared test helper: build @astryxdesign/core once, race-free.
5
+ *
6
+ * `astryx theme build` imports the compiled @astryxdesign/core/theme entry
7
+ * (there is no in-CLI fallback generator), so any test exercising it needs a
8
+ * built core. The CI `test` job runs `pnpm test` without a prior core build,
9
+ * and Vitest runs test files in parallel worker forks. When two build-theme
10
+ * suites each ran `if (!exists) pnpm -F @astryxdesign/core build` in their own
11
+ * beforeAll, both workers saw dist missing and launched concurrent builds that
12
+ * collided on the shared packages/core/dist (core's build starts with
13
+ * `rimraf dist`): one worker wiped dist while the other was mid-write, failing
14
+ * nondeterministically ("Could not resolve dist/index.js" / "ENOTEMPTY rmdir
15
+ * dist/hooks").
16
+ *
17
+ * This serializes the build behind a filesystem lock so exactly one worker
18
+ * builds and the rest wait for it to finish before reading dist.
19
+ */
20
+
21
+ import {execFileSync} from 'node:child_process';
22
+ import * as fs from 'node:fs';
23
+ import * as os from 'node:os';
24
+ import * as path from 'node:path';
25
+ import {fileURLToPath} from 'node:url';
26
+
27
+ const REPO_ROOT = path.resolve(
28
+ path.dirname(fileURLToPath(import.meta.url)),
29
+ '../../../..',
30
+ );
31
+ const CORE_THEME_ENTRY = path.join(
32
+ REPO_ROOT,
33
+ 'packages/core/dist/theme/index.js',
34
+ );
35
+ // A lock directory (mkdir is atomic across processes) guards the build.
36
+ const LOCK_DIR = path.join(os.tmpdir(), 'astryx-core-build.lock');
37
+
38
+ const BUILD_TIMEOUT_MS = 180_000;
39
+ // A lock older than this is assumed abandoned by a crashed/killed worker.
40
+ const STALE_LOCK_MS = BUILD_TIMEOUT_MS + 20_000;
41
+ const POLL_MS = 250;
42
+
43
+ function sleepSync(ms) {
44
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
45
+ }
46
+
47
+ function buildCore() {
48
+ execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
49
+ cwd: REPO_ROOT,
50
+ stdio: 'pipe',
51
+ timeout: BUILD_TIMEOUT_MS,
52
+ });
53
+ }
54
+
55
+ function lockIsStale() {
56
+ try {
57
+ return Date.now() - fs.statSync(LOCK_DIR).mtimeMs > STALE_LOCK_MS;
58
+ } catch {
59
+ // Vanished between the exists check and stat — treat as released.
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /** Try to acquire the lock. Returns true if this worker now holds it. */
65
+ function tryAcquire() {
66
+ try {
67
+ fs.mkdirSync(LOCK_DIR);
68
+ return true;
69
+ } catch (err) {
70
+ if (err.code !== 'EEXIST') {
71
+ throw err;
72
+ }
73
+ if (lockIsStale()) {
74
+ fs.rmSync(LOCK_DIR, {recursive: true, force: true});
75
+ try {
76
+ fs.mkdirSync(LOCK_DIR);
77
+ return true;
78
+ } catch (retryErr) {
79
+ if (retryErr.code !== 'EEXIST') {
80
+ throw retryErr;
81
+ }
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Ensure packages/core/dist is built exactly once, even when called
90
+ * concurrently from parallel Vitest workers. Safe to call from every
91
+ * build-theme suite's beforeAll.
92
+ */
93
+ export function ensureCoreBuilt() {
94
+ if (fs.existsSync(CORE_THEME_ENTRY)) {
95
+ return;
96
+ }
97
+
98
+ const deadline = Date.now() + STALE_LOCK_MS;
99
+ while (Date.now() < deadline) {
100
+ if (fs.existsSync(CORE_THEME_ENTRY)) {
101
+ return;
102
+ }
103
+ if (tryAcquire()) {
104
+ try {
105
+ if (!fs.existsSync(CORE_THEME_ENTRY)) {
106
+ buildCore();
107
+ }
108
+ } finally {
109
+ fs.rmSync(LOCK_DIR, {recursive: true, force: true});
110
+ }
111
+ return;
112
+ }
113
+ // Another worker is building; wait for it to finish and release the lock.
114
+ sleepSync(POLL_MS);
115
+ }
116
+
117
+ // Waited past the stale threshold without the artifact appearing — build it
118
+ // ourselves rather than let the suite fail on a missing entry.
119
+ buildCore();
120
+ }
@@ -91,15 +91,6 @@ describe('--json contract: rejects before side effects', () => {
91
91
  expect(fs.readdirSync(tmpDir)).toEqual([]);
92
92
  });
93
93
 
94
- it('astryx gap-report setup --json rejects before prompting', () => {
95
- const {status, stdout} = runCli(['gap-report', 'setup', '--json'], {cwd: tmpDir});
96
- expect(status).toBe(1);
97
- const parsed = parseJson(stdout);
98
- expect(parsed.error).toMatch(/gap-report setup/);
99
- // No astryx.config.mjs written.
100
- expect(fs.existsSync(path.join(tmpDir, 'astryx.config.mjs'))).toBe(false);
101
- });
102
-
103
94
  it('astryx theme --json (parent, no subcommand) rejects without printing help', () => {
104
95
  const {status, stdout, stderr} = runCli(['theme', '--json'], {cwd: tmpDir});
105
96
  expect(status).toBe(1);
@@ -145,29 +136,6 @@ describe('--json contract: supported commands emit valid envelopes', () => {
145
136
  expect(Array.isArray(parsed.data.jsonSupported)).toBe(true);
146
137
  });
147
138
 
148
- it('astryx gap-report --json --list-categories returns a typed envelope', () => {
149
- const {status, stdout} = runCli(['gap-report', '--json', '--list-categories'], {cwd: tmpDir});
150
- expect(status).toBe(0);
151
- const parsed = parseJson(stdout);
152
- expect(parsed.type).toBe('gap-report.categories');
153
- expect(Array.isArray(parsed.data)).toBe(true);
154
- });
155
-
156
- it('astryx gap-report --json without required flags emits a structured error (no clack)', () => {
157
- // Configure gap-report so it doesn't error on "disabled" — we want to
158
- // hit the "interactive prompt would start" branch.
159
- fs.writeFileSync(
160
- path.join(tmpDir, 'astryx.config.mjs'),
161
- 'export default { gapReport: { command: "true" } };\n',
162
- );
163
- const {status, stdout} = runCli(['gap-report', '--json'], {cwd: tmpDir});
164
- expect(status).toBe(1);
165
- const parsed = parseJson(stdout);
166
- expect(parsed.error).toMatch(/component/i);
167
- // Must not contain clack-rendered prompt characters.
168
- expect(stdout).not.toMatch(/Which component/);
169
- });
170
-
171
139
  it('astryx upgrade --json --list returns the codemod list', () => {
172
140
  const {status, stdout} = runCli(['upgrade', '--json', '--list'], {cwd: tmpDir});
173
141
  expect(status).toBe(0);