@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,91 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Pluggable config/discovery cache for the Project API.
5
+ *
6
+ * The Project class memoizes resolved discovery (components, templates,
7
+ * codemods, config) behind a small key/value cache so the same work is never
8
+ * repeated within a single Project instance. The cache interface is
9
+ * intentionally tiny — `get(key)` / `set(key, value)` — so a future
10
+ * file-backed, cross-process cache can drop in with NO API change.
11
+ *
12
+ * Cache keys are derived from a CONTENT HASH of the project's config file
13
+ * (see {@link configContentHash}) combined with the cwd and the
14
+ * discovery-kind (+ args). The in-memory implementation never busts on a
15
+ * content change, but the hash is computed regardless so a hash-busting,
16
+ * file-backed cache works against the exact same key path.
17
+ *
18
+ * @typedef {Object} ConfigCache
19
+ * @property {(key: string) => unknown} get
20
+ * @property {(key: string, value: unknown) => void} set
21
+ */
22
+
23
+ import * as fs from 'node:fs';
24
+ import * as crypto from 'node:crypto';
25
+
26
+ /**
27
+ * Stable sentinel hash used when there is no config file (or it can't be
28
+ * read). Keeps key derivation total — every cwd still produces a key.
29
+ */
30
+ export const NO_CONFIG_HASH = 'no-config';
31
+
32
+ /**
33
+ * Compute a stable content hash for a config file. Reads the file bytes and
34
+ * returns their sha256 hex digest; a missing/unreadable file yields
35
+ * {@link NO_CONFIG_HASH}. The hash is the bust signal a future file-backed
36
+ * cache would key on.
37
+ *
38
+ * @param {string | null | undefined} configPath absolute config path, or null
39
+ * @returns {string}
40
+ */
41
+ export function configContentHash(configPath) {
42
+ if (!configPath) return NO_CONFIG_HASH;
43
+ try {
44
+ const bytes = fs.readFileSync(configPath);
45
+ return crypto.createHash('sha256').update(bytes).digest('hex');
46
+ } catch {
47
+ return NO_CONFIG_HASH;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Build a cache key from a config content hash, a cwd, and a discovery kind
53
+ * (already including any args, e.g. `codemods:0.1.0..0.2.0`). The shape is
54
+ * stable so the same inputs always map to the same key.
55
+ *
56
+ * @param {string} hash config content hash
57
+ * @param {string} cwd
58
+ * @param {string} kind discovery kind (+args)
59
+ * @returns {string}
60
+ */
61
+ export function cacheKey(hash, cwd, kind) {
62
+ return `${hash}\u0000${cwd}\u0000${kind}`;
63
+ }
64
+
65
+ /**
66
+ * Default in-memory cache. Per-instance by default (the Project creates one in
67
+ * {@link Project.load} when none is supplied), so it behaves like simple
68
+ * memoization. A shared instance can be passed to reuse results across
69
+ * Projects. Never busts — a content change produces a different KEY (via the
70
+ * hash), not an eviction, which is exactly the behavior a future file-backed
71
+ * cache also relies on.
72
+ *
73
+ * @implements {ConfigCache}
74
+ */
75
+ export class InMemoryConfigCache {
76
+ /** @type {Map<string, unknown>} */
77
+ #m = new Map();
78
+
79
+ /** @param {string} key */
80
+ get(key) {
81
+ return this.#m.get(key);
82
+ }
83
+
84
+ /**
85
+ * @param {string} key
86
+ * @param {unknown} value
87
+ */
88
+ set(key, value) {
89
+ this.#m.set(key, value);
90
+ }
91
+ }
@@ -0,0 +1,83 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import {
7
+ InMemoryConfigCache,
8
+ cacheKey,
9
+ configContentHash,
10
+ NO_CONFIG_HASH,
11
+ } from './config-cache.mjs';
12
+
13
+ let tmpDir;
14
+
15
+ beforeEach(() => {
16
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-cache-test-'));
17
+ });
18
+
19
+ afterEach(() => {
20
+ fs.rmSync(tmpDir, {recursive: true, force: true});
21
+ });
22
+
23
+ describe('InMemoryConfigCache', () => {
24
+ it('stores and returns values by key', () => {
25
+ const cache = new InMemoryConfigCache();
26
+ expect(cache.get('a')).toBeUndefined();
27
+ cache.set('a', 42);
28
+ expect(cache.get('a')).toBe(42);
29
+ });
30
+
31
+ it('can store falsy values distinctly from a miss', () => {
32
+ const cache = new InMemoryConfigCache();
33
+ cache.set('zero', {value: 0});
34
+ expect(cache.get('zero')).toEqual({value: 0});
35
+ expect(cache.get('missing')).toBeUndefined();
36
+ });
37
+ });
38
+
39
+ describe('configContentHash', () => {
40
+ it('returns the sentinel when there is no config path', () => {
41
+ expect(configContentHash(null)).toBe(NO_CONFIG_HASH);
42
+ expect(configContentHash(undefined)).toBe(NO_CONFIG_HASH);
43
+ });
44
+
45
+ it('returns the sentinel for a missing file', () => {
46
+ expect(configContentHash(path.join(tmpDir, 'nope.mjs'))).toBe(
47
+ NO_CONFIG_HASH,
48
+ );
49
+ });
50
+
51
+ it('is stable for the same bytes', () => {
52
+ const file = path.join(tmpDir, 'astryx.config.mjs');
53
+ fs.writeFileSync(file, 'export default { integrations: [] };\n');
54
+ const a = configContentHash(file);
55
+ const b = configContentHash(file);
56
+ expect(a).toBe(b);
57
+ expect(a).not.toBe(NO_CONFIG_HASH);
58
+ });
59
+
60
+ it('differs when the config content changes', () => {
61
+ const file = path.join(tmpDir, 'astryx.config.mjs');
62
+ fs.writeFileSync(file, 'export default { integrations: [] };\n');
63
+ const before = configContentHash(file);
64
+ fs.writeFileSync(file, 'export default { integrations: ["@x/y"] };\n');
65
+ const after = configContentHash(file);
66
+ expect(before).not.toBe(after);
67
+ });
68
+ });
69
+
70
+ describe('cacheKey', () => {
71
+ it('is stable for identical inputs and varies by kind', () => {
72
+ const k1 = cacheKey('hash', '/cwd', 'components');
73
+ const k2 = cacheKey('hash', '/cwd', 'components');
74
+ const k3 = cacheKey('hash', '/cwd', 'templates');
75
+ expect(k1).toBe(k2);
76
+ expect(k1).not.toBe(k3);
77
+ });
78
+
79
+ it('varies by hash and cwd', () => {
80
+ expect(cacheKey('h1', '/cwd', 'k')).not.toBe(cacheKey('h2', '/cwd', 'k'));
81
+ expect(cacheKey('h', '/a', 'k')).not.toBe(cacheKey('h', '/b', 'k'));
82
+ });
83
+ });
@@ -8,77 +8,63 @@ const Fn = z.custom(value => typeof value === 'function', {
8
8
  message: 'Expected function',
9
9
  });
10
10
 
11
- const StringOrStringArray = z.union([z.string(), z.array(z.string())]);
12
- const GapReportSchema = z.union([
13
- z.literal(false),
14
- z.object({command: z.string()}).passthrough(),
15
- ]);
16
-
17
- export const AstryxConfigSchema = z
11
+ export const PostCodemodHookSchema = z
18
12
  .object({
19
- packages: StringOrStringArray.optional(),
20
- integrations: StringOrStringArray.optional(),
21
- gapReport: GapReportSchema.optional(),
22
- template: z
23
- .object({
24
- get: Fn.optional(),
25
- })
26
- .passthrough()
27
- .optional(),
13
+ name: z.string().optional(),
14
+ buildCommand: Fn,
28
15
  })
29
- .passthrough();
16
+ .strict();
30
17
 
31
- export const AstryxIntegrationCodemodSchema = z
18
+ export const XleComponentSchema = z
32
19
  .object({
33
- name: z.string(),
34
- from: z.string().optional(),
35
- to: z.string().optional(),
36
- title: z.string().optional(),
20
+ from: z.string(),
37
21
  description: z.string().optional(),
38
- pr: z.string().optional(),
39
- optional: z.boolean().optional(),
40
- fileExtensions: z.array(z.string()).optional(),
41
- transform: z.union([z.string(), Fn]),
22
+ default: z.boolean().optional(),
42
23
  })
43
- .passthrough();
24
+ .strict();
44
25
 
45
- export const AstryxPostCodemodHookSchema = z
26
+ export const AstryxConfigSchema = z
46
27
  .object({
47
- name: z.string().optional(),
48
- run: Fn.optional(),
49
- command: Fn.optional(),
28
+ integrations: z.array(z.string()).optional(),
29
+ issuesUrl: z.string().url().optional(),
30
+ hooks: z
31
+ .object({
32
+ postCodemod: z.array(PostCodemodHookSchema).optional(),
33
+ })
34
+ .strict()
35
+ .optional(),
36
+ experimental: z
37
+ .object({
38
+ xle: z
39
+ .object({
40
+ components: z.record(z.string(), XleComponentSchema).optional(),
41
+ })
42
+ .strict()
43
+ .optional(),
44
+ })
45
+ .strict()
46
+ .optional(),
50
47
  })
51
- .passthrough()
52
- .refine(value => value.run || value.command, {
53
- message: 'postCodemod hook must define run() or command()',
54
- });
48
+ .strict();
55
49
 
56
50
  export const AstryxIntegrationSchema = z
57
51
  .object({
58
- name: z.string(),
59
- version: z.string().optional(),
60
- displayName: z.string().optional(),
61
- description: z.string().optional(),
62
- docs: z.string().optional(),
63
- category: z.string().optional(),
64
- blocks: z.string().optional(),
65
- gapReport: GapReportSchema.optional(),
66
- template: z
67
- .object({
68
- get: z.union([z.string(), Fn]).optional(),
69
- })
70
- .passthrough()
71
- .optional(),
72
- codemods: z.array(AstryxIntegrationCodemodSchema).optional(),
73
- postCodemod: z.array(AstryxPostCodemodHookSchema).optional(),
52
+ components: z.string().optional(),
53
+ templates: z.string().optional(),
54
+ codemods: z.string().optional(),
55
+ issuesUrl: z.string().url().optional(),
74
56
  })
75
- .passthrough();
57
+ .strict();
76
58
 
77
59
  /**
60
+ * Format a zod error into a single readable line, mirroring the issue-joining
61
+ * convention used across the CLI (path: message; path: message). Exported so
62
+ * the shared module loader and other validators stay in lockstep.
78
63
  * @param {string} label
79
64
  * @param {import('zod').ZodError} error
65
+ * @returns {string}
80
66
  */
81
- function formatZodError(label, error) {
67
+ export function formatZodError(label, error) {
82
68
  const issues = error.issues
83
69
  .map(issue => {
84
70
  const path = issue.path.length ? issue.path.join('.') : '(root)';
@@ -96,7 +82,7 @@ export function validateConfig(config) {
96
82
  const result = AstryxConfigSchema.safeParse(config);
97
83
  if (!result.success) {
98
84
  throw new Error(
99
- formatZodError('astryx.config.mjs default export', result.error),
85
+ formatZodError('astryx.config default export', result.error),
100
86
  );
101
87
  }
102
88
  return result.data;
@@ -105,7 +91,7 @@ export function validateConfig(config) {
105
91
  /**
106
92
  * @param {unknown} integration
107
93
  * @param {string} [label]
108
- * @returns {import('../types/config').AstryxIntegration}
94
+ * @returns {import('../types/integration').AstryxIntegration}
109
95
  */
110
96
  export function validateIntegration(
111
97
  integration,
@@ -52,6 +52,8 @@
52
52
  * | 'ERR_UNKNOWN_SECTION'
53
53
  * | 'ERR_UNKNOWN_CATEGORY'
54
54
  * | 'ERR_UNKNOWN_TEMPLATE'
55
+ * | 'ERR_AMBIGUOUS_TEMPLATE'
56
+ * | 'ERR_AMBIGUOUS_COMPONENT'
55
57
  * | 'ERR_UNKNOWN_THEME'
56
58
  * | 'ERR_UNKNOWN_PACKAGE'
57
59
  * | 'ERR_UNKNOWN_AGENT'
@@ -69,13 +71,12 @@
69
71
  * | 'ERR_WRITE_FAILED'
70
72
  * | 'ERR_THEME_INVALID'
71
73
  * | 'ERR_THEME_LOAD'
72
- * | 'ERR_TEMPLATE_CONFIG'
73
- * | 'ERR_TEMPLATE_GET'
74
74
  * | 'ERR_VERSION_DETECT'
75
75
  * | 'ERR_INVALID_VERSION'
76
76
  * | 'ERR_DEP_MISSING'
77
77
  * | 'ERR_GH_CLI'
78
- * | 'ERR_GAP_REPORT_FAILED'
78
+ * | 'ERR_UNKNOWN_POST'
79
+ * | 'ERR_FETCH_FAILED'
79
80
  * | 'ERR_LAYOUT_PARSE'
80
81
  * | 'ERR_LAYOUT_INVALID'
81
82
  * )} ErrorCode
@@ -125,6 +126,10 @@ export const ERROR_CODES = Object.freeze({
125
126
  ERR_UNKNOWN_CATEGORY: 'ERR_UNKNOWN_CATEGORY',
126
127
  /** No template matched the requested name. */
127
128
  ERR_UNKNOWN_TEMPLATE: 'ERR_UNKNOWN_TEMPLATE',
129
+ /** A template id matched more than one template (narrow with --type/--package). */
130
+ ERR_AMBIGUOUS_TEMPLATE: 'ERR_AMBIGUOUS_TEMPLATE',
131
+ /** A component name is owned by more than one package (narrow with --package). */
132
+ ERR_AMBIGUOUS_COMPONENT: 'ERR_AMBIGUOUS_COMPONENT',
128
133
  /** No theme matched the requested slug (theme add). */
129
134
  ERR_UNKNOWN_THEME: 'ERR_UNKNOWN_THEME',
130
135
  /** No package matched the requested name (discover). */
@@ -166,12 +171,6 @@ export const ERROR_CODES = Object.freeze({
166
171
  /** A theme file could not be loaded / parsed into a defineTheme result. */
167
172
  ERR_THEME_LOAD: 'ERR_THEME_LOAD',
168
173
 
169
- // ── Template config ──────────────────────────────────────────────
170
- /** `template.get` is not configured in astryx.config.mjs (fetch-by-id). */
171
- ERR_TEMPLATE_CONFIG: 'ERR_TEMPLATE_CONFIG',
172
- /** A configured `template.get` threw or returned an invalid value. */
173
- ERR_TEMPLATE_GET: 'ERR_TEMPLATE_GET',
174
-
175
174
  // ── Upgrade ──────────────────────────────────────────────────────
176
175
  /** The current `@astryxdesign/core` version could not be detected. */
177
176
  ERR_VERSION_DETECT: 'ERR_VERSION_DETECT',
@@ -180,11 +179,15 @@ export const ERROR_CODES = Object.freeze({
180
179
  /** A required external dependency (e.g. jscodeshift) is missing. */
181
180
  ERR_DEP_MISSING: 'ERR_DEP_MISSING',
182
181
 
183
- // ── Gap report ───────────────────────────────────────────────────
182
+ // ── GitHub CLI ───────────────────────────────────────────────────
184
183
  /** GitHub CLI (`gh`) is not installed or not authenticated. */
185
184
  ERR_GH_CLI: 'ERR_GH_CLI',
186
- /** Filing a gap report failed at the command/integration boundary. */
187
- ERR_GAP_REPORT_FAILED: 'ERR_GAP_REPORT_FAILED',
185
+
186
+ // ── Blog (read via the published RSS feed) ───────────────────────
187
+ /** No blog post matched the requested slug in the feed. */
188
+ ERR_UNKNOWN_POST: 'ERR_UNKNOWN_POST',
189
+ /** A network fetch (RSS feed or post text) failed. */
190
+ ERR_FETCH_FAILED: 'ERR_FETCH_FAILED',
188
191
 
189
192
  // ── Layout expressions (XLE/XLO) ─────────────────────────────────
190
193
  /** A layout expression failed to parse (syntax error, with line/col). */
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Compact, non-blocking integration-issue nudge for everyday commands.
5
+ *
6
+ * When a CONFIGURED integration (from the Project's `loadedIntegrations`) has
7
+ * validation issues, the everyday commands (component / template / upgrade)
8
+ * should print ONE compact, non-blocking line per integration telling the user
9
+ * to run `validate-integration` — instead of silently skipping broken
10
+ * contributions or spamming per-contribution diagnostics.
11
+ *
12
+ * Design constraints (all enforced here):
13
+ * - Reuses the validate-integration validators (validateLoadedIntegration);
14
+ * no validation logic is duplicated.
15
+ * - Writes to STDERR only, so it never corrupts a --json stdout envelope.
16
+ * - Suppressed entirely in --json mode.
17
+ * - Best-effort: never throws, never changes the exit code. Broken
18
+ * contributions are still skipped downstream exactly as before; this only
19
+ * ADDS a one-line nudge.
20
+ */
21
+
22
+ import {validateLoadedIntegration} from '../api/validate-integration.mjs';
23
+
24
+ /**
25
+ * For each configured (already-loaded) integration, compute its issues using
26
+ * the shared validate-integration validators and, if any exist, print exactly
27
+ * ONE line per integration to stderr:
28
+ *
29
+ * Warning: <pkg> has N integration issue(s). Run: astryx validate-integration <pkg>
30
+ *
31
+ * @param {Array<object>} loadedIntegrations the Project's loaded integrations
32
+ * @param {{json?: boolean}} [options]
33
+ * @returns {Promise<void>}
34
+ */
35
+ export async function warnOnIntegrationIssues(loadedIntegrations, {json = false} = {}) {
36
+ try {
37
+ // Cheap guard: only do work when integrations are actually configured.
38
+ if (json) return;
39
+ if (!Array.isArray(loadedIntegrations) || loadedIntegrations.length === 0) {
40
+ return;
41
+ }
42
+ for (const integration of loadedIntegrations) {
43
+ if (!integration || typeof integration !== 'object') continue;
44
+ let issues;
45
+ try {
46
+ issues = await validateLoadedIntegration(integration);
47
+ } catch {
48
+ // Best-effort: a validator throwing must not break the host command.
49
+ continue;
50
+ }
51
+ if (!Array.isArray(issues) || issues.length === 0) continue;
52
+ const pkg = integration.name ?? integration.__spec ?? '(integration)';
53
+ // Stderr only — keeps stdout (and any --json envelope) clean.
54
+ console.error(
55
+ `Warning: ${pkg} has ${issues.length} integration issue(s). ` +
56
+ `Run: astryx validate-integration ${pkg}`,
57
+ );
58
+ }
59
+ } catch {
60
+ // Never throw, never change the exit code.
61
+ }
62
+ }
@@ -0,0 +1,102 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Hermetic tests for the compact integration-issue nudge.
5
+ *
6
+ * Builds loaded-integration-shaped objects (as produced by
7
+ * lib/integrations.mjs) with absolute contribution roots under the repo root
8
+ * (process.cwd()) so any node_modules-style path stays within Vite's allowed
9
+ * fs roots. We never load configs from /tmp. The helper is exercised directly;
10
+ * stderr is captured to assert exactly one warning line per broken integration
11
+ * and zero output in --json mode / for clean integrations.
12
+ */
13
+
14
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import {warnOnIntegrationIssues} from './integration-warnings.mjs';
18
+
19
+ let tmpDir;
20
+ let errSpy;
21
+ let errLines;
22
+
23
+ beforeEach(() => {
24
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-warn-it-'));
25
+ errLines = [];
26
+ errSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
27
+ errLines.push(args.join(' '));
28
+ });
29
+ });
30
+
31
+ afterEach(() => {
32
+ errSpy.mockRestore();
33
+ fs.rmSync(tmpDir, {recursive: true, force: true});
34
+ });
35
+
36
+ /**
37
+ * Build a loaded-integration-shaped object. Roots are absolute (mirrors
38
+ * loadIntegrations' resolveRoot).
39
+ */
40
+ function loaded({name = '@acme/widgets', components, templates, codemods} = {}) {
41
+ return {
42
+ name,
43
+ version: '1.0.0',
44
+ components: components == null ? undefined : path.join(tmpDir, components),
45
+ templates: templates == null ? undefined : path.join(tmpDir, templates),
46
+ codemods: codemods == null ? undefined : path.join(tmpDir, codemods),
47
+ __spec: name,
48
+ __packageDir: tmpDir,
49
+ };
50
+ }
51
+
52
+ describe('warnOnIntegrationIssues', () => {
53
+ it('emits exactly one stderr line for an integration with issues', async () => {
54
+ // Declared components root that does not exist on disk → missing_root.
55
+ const integration = loaded({components: 'does-not-exist'});
56
+ await warnOnIntegrationIssues([integration], {json: false});
57
+
58
+ expect(errLines).toHaveLength(1);
59
+ expect(errLines[0]).toBe(
60
+ 'Warning: @acme/widgets has 1 integration issue(s). ' +
61
+ 'Run: astryx validate-integration @acme/widgets',
62
+ );
63
+ });
64
+
65
+ it('emits nothing in --json mode (keeps stdout/JSON clean)', async () => {
66
+ const integration = loaded({components: 'does-not-exist'});
67
+ await warnOnIntegrationIssues([integration], {json: true});
68
+ expect(errLines).toHaveLength(0);
69
+ });
70
+
71
+ it('emits nothing for an integration with no issues', async () => {
72
+ // Existing components root with no broken contributions → no issues.
73
+ const componentsRoot = path.join(tmpDir, 'components');
74
+ fs.mkdirSync(componentsRoot, {recursive: true});
75
+ const integration = loaded({components: 'components'});
76
+ await warnOnIntegrationIssues([integration], {json: false});
77
+ expect(errLines).toHaveLength(0);
78
+ });
79
+
80
+ it('emits nothing when there are no configured integrations', async () => {
81
+ await warnOnIntegrationIssues([], {json: false});
82
+ await warnOnIntegrationIssues(undefined, {json: false});
83
+ expect(errLines).toHaveLength(0);
84
+ });
85
+
86
+ it('emits one line per broken integration', async () => {
87
+ const a = loaded({name: '@acme/a', components: 'missing-a'});
88
+ const b = loaded({name: '@acme/b', templates: 'missing-b'});
89
+ await warnOnIntegrationIssues([a, b], {json: false});
90
+ expect(errLines).toHaveLength(2);
91
+ expect(errLines[0]).toContain('@acme/a');
92
+ expect(errLines[1]).toContain('@acme/b');
93
+ });
94
+
95
+ it('never throws and never emits when a validator misbehaves', async () => {
96
+ // A non-object entry must be skipped silently.
97
+ await expect(
98
+ warnOnIntegrationIssues([null, 'nope', 42], {json: false}),
99
+ ).resolves.toBeUndefined();
100
+ expect(errLines).toHaveLength(0);
101
+ });
102
+ });