@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
@@ -3,95 +3,89 @@
3
3
  /**
4
4
  * @file Integration manifest loading for Astryx config.
5
5
  *
6
- * Integrations are package names or direct manifest paths listed in
7
- * astryx.config.mjs. A manifest can contribute docs/discovery metadata,
8
- * gap-report/template hooks, upgrade codemods, and post-codemod hooks.
6
+ * Integrations are PACKAGE NAMES listed in astryx.config.{ts,mjs,js}. Each
7
+ * package declares a single conventional root manifest sibling to its
8
+ * package.json astryx.integration.{ts,mjs,js} which contributes
9
+ * components/templates/codemods roots and an optional issuesUrl. Identity
10
+ * (name, version) comes from the package's package.json, not the manifest.
9
11
  */
10
12
 
11
13
  import * as fs from 'node:fs';
12
14
  import * as path from 'node:path';
13
- import {pathToFileURL} from 'node:url';
14
- import {validateIntegration} from './config-schema.mjs';
15
+ import {AstryxIntegrationSchema} from './config-schema.mjs';
16
+ import {loadModuleWithSchema, findPresentFiles} from './module-loader.mjs';
15
17
 
16
- export function isPathSpec(spec) {
17
- return (
18
- spec.startsWith('.') ||
19
- spec.startsWith('/') ||
20
- spec.endsWith('.mjs') ||
21
- spec.endsWith('.js')
22
- );
18
+ /** Conventional manifest basenames, in load-precedence order. */
19
+ export const MANIFEST_BASENAMES = [
20
+ 'astryx.integration.ts',
21
+ 'astryx.integration.mjs',
22
+ 'astryx.integration.js',
23
+ ];
24
+
25
+ /**
26
+ * Return the conventional root manifest paths present in `dir`, in
27
+ * load-precedence order. Unlike {@link resolveManifestPath} this never throws —
28
+ * callers (e.g. validate-integration) decide how to treat zero / multiple.
29
+ * @param {string} dir
30
+ * @returns {string[]} absolute manifest paths
31
+ */
32
+ export function findManifestPaths(dir) {
33
+ return findPresentFiles(dir, MANIFEST_BASENAMES);
23
34
  }
24
35
 
36
+ /**
37
+ * Load and validate a manifest module's default export against the integration
38
+ * schema. Default export only — `.ts` is loaded via jiti; `.mjs`/`.js` via
39
+ * dynamic import. Throws if the default export is missing or invalid. Exposed
40
+ * for validate-integration.
41
+ * @param {string} file absolute manifest path
42
+ * @param {string} [label] used in error messages
43
+ * @returns {Promise<import('../types/integration').AstryxIntegration>}
44
+ */
45
+ export async function loadManifestObject(file, label = 'integration manifest') {
46
+ return loadModuleWithSchema(file, AstryxIntegrationSchema, {label});
47
+ }
48
+
49
+ /**
50
+ * Resolve the directory of an installed integration package from the consumer's
51
+ * node_modules.
52
+ * @param {string} packageName
53
+ * @param {string} [cwd]
54
+ */
25
55
  export function resolvePackageDir(packageName, cwd = process.cwd()) {
26
56
  return path.resolve(cwd, 'node_modules', ...packageName.split('/'));
27
57
  }
28
58
 
29
- export function resolveIntegrationFile(spec, cwd = process.cwd()) {
30
- if (isPathSpec(spec)) {
31
- return path.resolve(cwd, spec);
32
- }
33
-
34
- const packageDir = resolvePackageDir(spec, cwd);
35
- const pkgPath = path.join(packageDir, 'package.json');
36
- let pkg;
37
- try {
38
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
39
- } catch {
59
+ /**
60
+ * Find the single conventional root manifest sibling of the package's
61
+ * package.json. Multiple present is a hard error; missing is a hard error.
62
+ * @param {string} packageDir
63
+ * @param {string} spec
64
+ * @returns {string} absolute manifest path
65
+ */
66
+ function resolveManifestPath(packageDir, spec) {
67
+ const present = findPresentFiles(packageDir, MANIFEST_BASENAMES);
68
+ if (present.length === 0) {
40
69
  throw new Error(
41
- `Could not find installed integration package "${spec}" at ${pkgPath}. Install it first or pass a direct integration file path.`,
70
+ `Integration package "${spec}" has no conventional root manifest. Add one of: ${MANIFEST_BASENAMES.join(', ')} next to its package.json.`,
42
71
  );
43
72
  }
44
-
45
- const manifestPath = pkg.astryx?.integration ?? pkg.xds?.integration;
46
- if (!manifestPath) {
73
+ if (present.length > 1) {
47
74
  throw new Error(
48
- `Package "${spec}" does not declare astryx.integration (or legacy xds.integration) in package.json.`,
75
+ `Integration package "${spec}" has multiple root manifests (${present
76
+ .map(file => path.basename(file))
77
+ .join(', ')}). Keep exactly one.`,
49
78
  );
50
79
  }
51
- return path.resolve(packageDir, manifestPath);
52
- }
53
-
54
- function normalizePackageContribution(integration) {
55
- const docs = integration.docs;
56
- if (!docs) return null;
57
- const packageDir = integration.__packageDir ?? integration.__dir;
58
- const docsDir = path.resolve(packageDir, docs);
59
- return {
60
- name: integration.name ?? integration.__spec,
61
- version: integration.version,
62
- description: integration.description,
63
- displayName: integration.displayName,
64
- dir: packageDir,
65
- astryx: {
66
- docs,
67
- category: integration.category,
68
- blocks: integration.blocks,
69
- },
70
- category: integration.category ?? integration.name ?? integration.__spec,
71
- docsDir,
72
- };
73
- }
74
-
75
- function resolveHookFunction(hook, integration) {
76
- if (typeof hook === 'function') return hook;
77
- if (typeof hook !== 'string') return hook;
78
- const [moduleSpec, exportName = 'default'] = hook.split('#');
79
- return async (...args) => {
80
- const mod = isPathSpec(moduleSpec)
81
- ? await import(
82
- pathToFileURL(path.resolve(integration.__dir, moduleSpec)).href
83
- )
84
- : await import(moduleSpec);
85
- const fn = exportName === 'default' ? mod.default : mod[exportName];
86
- if (typeof fn !== 'function') {
87
- throw new Error(
88
- `Integration hook ${hook} did not resolve to a function.`,
89
- );
90
- }
91
- return fn(...args);
92
- };
80
+ return present[0];
93
81
  }
94
82
 
83
+ /**
84
+ * Load configured integrations.
85
+ *
86
+ * @param {string[]} [specs] package names
87
+ * @param {{cwd?: string}} [options]
88
+ */
95
89
  export async function loadIntegrations(specs = [], {cwd = process.cwd()} = {}) {
96
90
  const integrations = [];
97
91
  const seen = new Set();
@@ -100,55 +94,38 @@ export async function loadIntegrations(specs = [], {cwd = process.cwd()} = {}) {
100
94
  if (!spec || seen.has(spec)) continue;
101
95
  seen.add(spec);
102
96
 
103
- const file = resolveIntegrationFile(spec, cwd);
104
- const mod = await import(pathToFileURL(file).href);
105
- const exported = mod.default ?? mod.integration ?? mod;
106
- if (!exported || typeof exported !== 'object') {
107
- throw new Error(`Integration ${spec} did not export an object.`);
97
+ const packageDir = resolvePackageDir(spec, cwd);
98
+ const pkgPath = path.join(packageDir, 'package.json');
99
+ let pkg;
100
+ try {
101
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
102
+ } catch {
103
+ throw new Error(
104
+ `Could not find installed integration package "${spec}" at ${pkgPath}. Install it first.`,
105
+ );
108
106
  }
109
- const integration = validateIntegration(exported, `Integration ${spec}`);
110
107
 
111
- const integrationDir = path.dirname(file);
112
- const packageDir = isPathSpec(spec)
113
- ? integrationDir
114
- : resolvePackageDir(spec, cwd);
115
- const normalized = {
116
- ...integration,
117
- __file: file,
118
- __dir: integrationDir,
119
- __packageDir: packageDir,
120
- __spec: spec,
121
- };
122
-
123
- if (Array.isArray(normalized.codemods)) {
124
- for (const codemod of normalized.codemods) {
125
- if (typeof codemod.transform === 'string') {
126
- const transformPath = path.resolve(integrationDir, codemod.transform);
127
- const transformMod = await import(pathToFileURL(transformPath).href);
128
- codemod.transform =
129
- transformMod.default ?? transformMod.transform ?? transformMod;
130
- }
131
- }
132
- }
108
+ const manifestFile = resolveManifestPath(packageDir, spec);
109
+ const manifest = await loadModuleWithSchema(
110
+ manifestFile,
111
+ AstryxIntegrationSchema,
112
+ {label: `Integration ${spec}`},
113
+ );
133
114
 
134
- normalized.package = normalizePackageContribution(normalized);
135
- if (
136
- normalized.gapReport?.command &&
137
- isPathSpec(normalized.gapReport.command)
138
- ) {
139
- normalized.gapReport = {
140
- ...normalized.gapReport,
141
- command: path.resolve(packageDir, normalized.gapReport.command),
142
- };
143
- }
144
- if (normalized.template?.get) {
145
- normalized.template = {
146
- ...normalized.template,
147
- get: resolveHookFunction(normalized.template.get, normalized),
148
- };
149
- }
115
+ const resolveRoot = value =>
116
+ value == null ? undefined : path.resolve(packageDir, value);
150
117
 
151
- integrations.push(normalized);
118
+ integrations.push({
119
+ name: pkg.name ?? spec,
120
+ version: pkg.version,
121
+ components: resolveRoot(manifest.components),
122
+ templates: resolveRoot(manifest.templates),
123
+ codemods: resolveRoot(manifest.codemods),
124
+ issuesUrl: manifest.issuesUrl,
125
+ __spec: spec,
126
+ __packageDir: packageDir,
127
+ __manifestFile: manifestFile,
128
+ });
152
129
  }
153
130
 
154
131
  return integrations;
@@ -3,91 +3,39 @@
3
3
  import {afterEach, beforeEach, describe, expect, it} from 'vitest';
4
4
  import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
- import {loadConfig} from './config.mjs';
6
+ import {Project} from './project.mjs';
7
+ import {loadIntegrations} from './integrations.mjs';
7
8
  import {discover} from '../api/discover.mjs';
8
- import {loadGapReportConfig} from '../utils/github.mjs';
9
- import {getTemplateById} from '../api/template.mjs';
10
9
 
11
10
  let tmpDir;
12
11
  let originalCwd;
13
12
 
14
- beforeEach(() => {
15
- originalCwd = process.cwd();
16
- tmpDir = fs.mkdtempSync(
17
- path.join(process.cwd(), '.astryx-integration-test-'),
18
- );
19
- fs.mkdirSync(path.join(tmpDir, 'node_modules', '@acme', 'widgets'), {
20
- recursive: true,
21
- });
13
+ function writeManifestPackage(dir, {basename = 'astryx.integration.mjs', body}) {
14
+ const pkgDir = path.join(dir, 'node_modules', '@acme', 'widgets');
15
+ fs.mkdirSync(pkgDir, {recursive: true});
22
16
  fs.writeFileSync(
23
- path.join(tmpDir, 'astryx.config.mjs'),
24
- `export default { integrations: '@acme/widgets' };\n`,
25
- );
26
- fs.writeFileSync(
27
- path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'package.json'),
17
+ path.join(pkgDir, 'package.json'),
28
18
  JSON.stringify({
29
19
  name: '@acme/widgets',
30
20
  version: '1.2.3',
31
- displayName: 'Acme Widgets',
32
- astryx: {integration: './astryx.integration.mjs'},
33
21
  }),
34
22
  );
35
- fs.writeFileSync(
36
- path.join(
37
- tmpDir,
38
- 'node_modules',
39
- '@acme',
40
- 'widgets',
41
- 'astryx.integration.mjs',
42
- ),
43
- `export default {
44
- name: '@acme/widgets',
45
- version: '1.2.3',
46
- displayName: 'Acme Widgets',
47
- docs: './docs',
48
- category: 'Acme',
49
- gapReport: {command: './scripts/report-gap.sh'},
50
- template: {get: './template.mjs#getTemplate'},
51
- codemods: [{name: 'noop', from: '0.0.0', to: '9.0.0', transform: './codemod.mjs'}],
52
- };\n`,
53
- );
54
- fs.mkdirSync(path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'docs'));
55
- fs.writeFileSync(
56
- path.join(
57
- tmpDir,
58
- 'node_modules',
59
- '@acme',
60
- 'widgets',
61
- 'docs',
62
- 'Widget.doc.mjs',
63
- ),
64
- `export const doc = {
65
- name: 'Widget',
66
- usage: {description: 'Acme widget'},
67
- props: [],
68
- };\n`,
69
- );
70
- fs.mkdirSync(
71
- path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'scripts'),
72
- );
73
- fs.writeFileSync(
74
- path.join(
75
- tmpDir,
76
- 'node_modules',
77
- '@acme',
78
- 'widgets',
79
- 'scripts',
80
- 'report-gap.sh',
81
- ),
82
- '#!/bin/sh\ncat >/dev/null\necho ok\n',
23
+ fs.writeFileSync(path.join(pkgDir, basename), body);
24
+ return pkgDir;
25
+ }
26
+
27
+ beforeEach(() => {
28
+ originalCwd = process.cwd();
29
+ tmpDir = fs.mkdtempSync(
30
+ path.join(process.cwd(), '.astryx-integration-test-'),
83
31
  );
84
32
  fs.writeFileSync(
85
- path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'template.mjs'),
86
- `export async function getTemplate(id) { return 'template:' + id; }\n`,
33
+ path.join(tmpDir, 'package.json'),
34
+ JSON.stringify({name: 'consumer'}),
87
35
  );
88
36
  fs.writeFileSync(
89
- path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'codemod.mjs'),
90
- `export default function transform() { return undefined; }\n`,
37
+ path.join(tmpDir, 'astryx.config.mjs'),
38
+ `export default { integrations: ['@acme/widgets'] };\n`,
91
39
  );
92
40
  process.chdir(tmpDir);
93
41
  });
@@ -98,57 +46,90 @@ afterEach(() => {
98
46
  });
99
47
 
100
48
  describe('configured integrations', () => {
101
- it('load docs/gap/template/codemod metadata from integration manifests', async () => {
102
- const config = await loadConfig(tmpDir);
103
- expect(config.integrations).toEqual(['@acme/widgets']);
104
- expect(config.loadedIntegrations[0].name).toBe('@acme/widgets');
105
- expect(config.loadedIntegrations[0].package).toMatchObject({
106
- name: '@acme/widgets',
107
- category: 'Acme',
49
+ it('resolves identity from package.json and contribution roots to absolute paths', async () => {
50
+ const pkgDir = writeManifestPackage(tmpDir, {
51
+ body: `export default {
52
+ components: './docs',
53
+ templates: './blocks',
54
+ codemods: './codemods',
55
+ issuesUrl: 'https://example.com/issues',
56
+ };\n`,
108
57
  });
109
- expect(config.loadedIntegrations[0].codemods[0].transform).toEqual(
110
- expect.any(Function),
111
- );
112
- expect(config.gapReport.command).toBe(
113
- path.join(
114
- tmpDir,
115
- 'node_modules',
116
- '@acme',
117
- 'widgets',
118
- 'scripts',
119
- 'report-gap.sh',
120
- ),
58
+ fs.mkdirSync(path.join(pkgDir, 'docs'));
59
+ fs.writeFileSync(
60
+ path.join(pkgDir, 'docs', 'Widget.doc.mjs'),
61
+ `export const doc = {
62
+ name: 'Widget',
63
+ usage: {description: 'Acme widget'},
64
+ props: [],
65
+ };\n`,
121
66
  );
122
- await expect(config.template.get('P123')).resolves.toBe('template:P123');
67
+
68
+ const project = await Project.load(tmpDir);
69
+ expect(project.integrations).toEqual(['@acme/widgets']);
70
+ const loaded = project.loadedIntegrations[0];
71
+ // Identity comes from package.json, not the manifest.
72
+ expect(loaded.name).toBe('@acme/widgets');
73
+ expect(loaded.version).toBe('1.2.3');
74
+ expect(loaded.components).toBe(path.join(pkgDir, 'docs'));
75
+ expect(loaded.templates).toBe(path.join(pkgDir, 'blocks'));
76
+ expect(loaded.codemods).toBe(path.join(pkgDir, 'codemods'));
77
+ expect(loaded.issuesUrl).toBe('https://example.com/issues');
123
78
  });
124
79
 
125
- it('makes integration docs discoverable without packages config', async () => {
80
+ it('makes integration components discoverable', async () => {
81
+ const pkgDir = writeManifestPackage(tmpDir, {
82
+ body: `export default { components: './docs' };\n`,
83
+ });
84
+ fs.mkdirSync(path.join(pkgDir, 'docs'));
85
+ fs.writeFileSync(
86
+ path.join(pkgDir, 'docs', 'Widget.doc.mjs'),
87
+ `export const doc = {
88
+ name: 'Widget',
89
+ usage: {description: 'Acme widget'},
90
+ props: [],
91
+ };\n`,
92
+ );
93
+
126
94
  const result = await discover(undefined, {});
127
95
  expect(result.type).toBe('discover.list');
128
96
  expect(result.data).toEqual([
129
97
  expect.objectContaining({
130
98
  name: '@acme/widgets',
131
- category: 'Acme',
99
+ category: '@acme/widgets',
132
100
  components: ['Widget'],
133
101
  }),
134
102
  ]);
135
103
  });
136
104
 
137
- it('uses integration gap-report and template hooks', async () => {
138
- await expect(loadGapReportConfig()).resolves.toMatchObject({
139
- enabled: true,
140
- command: path.join(
141
- tmpDir,
142
- 'node_modules',
143
- '@acme',
144
- 'widgets',
145
- 'scripts',
146
- 'report-gap.sh',
147
- ),
148
- });
149
- await expect(getTemplateById('P123', {cwd: tmpDir})).resolves.toEqual({
150
- type: 'template.get',
151
- data: {id: 'P123', source: 'template:P123'},
105
+ it('errors when the package has no conventional root manifest', async () => {
106
+ const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
107
+ fs.mkdirSync(pkgDir, {recursive: true});
108
+ fs.writeFileSync(
109
+ path.join(pkgDir, 'package.json'),
110
+ JSON.stringify({name: '@acme/widgets'}),
111
+ );
112
+ await expect(loadIntegrations(['@acme/widgets'], {cwd: tmpDir})).rejects.toThrow(
113
+ /no conventional root manifest/,
114
+ );
115
+ });
116
+
117
+ it('errors when the package has multiple root manifests', async () => {
118
+ const pkgDir = writeManifestPackage(tmpDir, {
119
+ body: `export default {};\n`,
152
120
  });
121
+ fs.writeFileSync(
122
+ path.join(pkgDir, 'astryx.integration.js'),
123
+ `module.exports = {};\n`,
124
+ );
125
+ await expect(loadIntegrations(['@acme/widgets'], {cwd: tmpDir})).rejects.toThrow(
126
+ /multiple root manifests/,
127
+ );
128
+ });
129
+
130
+ it('errors when the package is not installed', async () => {
131
+ await expect(loadIntegrations(['@acme/missing'], {cwd: tmpDir})).rejects.toThrow(
132
+ /Could not find installed integration package/,
133
+ );
153
134
  });
154
135
  });
@@ -56,16 +56,15 @@ export const RESPONSE_TYPES = {
56
56
  'template.show',
57
57
  'template.skeleton',
58
58
  'template.copy',
59
- 'template.get',
60
59
  ],
61
60
  hook: ['hook.list', 'hook.brief', 'hook.full', 'hook.detail', 'hook.detail.params'],
62
61
  'theme build': ['theme.build'],
63
62
  'theme list': ['theme.list'],
64
63
  'theme add': ['theme.list', 'theme.add'],
65
64
  upgrade: ['upgrade.list', 'upgrade.status', 'upgrade.run'],
66
- 'gap-report': ['gap-report.categories', 'gap-report.dryRun', 'gap-report.file'],
67
65
  manifest: ['manifest'],
68
66
  doctor: ['doctor'],
67
+ 'validate-integration': ['integration.validate'],
69
68
  'layout expand': ['layout.expand'],
70
69
  'layout check': ['layout.check'],
71
70
  'layout grammar': ['layout.grammar'],
@@ -89,6 +88,10 @@ const EXAMPLES = {
89
88
  upgrade: ['astryx upgrade --json'],
90
89
  manifest: ['astryx manifest --json', 'astryx --json'],
91
90
  doctor: ['astryx doctor', 'astryx doctor --json'],
91
+ 'validate-integration': [
92
+ 'astryx validate-integration',
93
+ 'astryx validate-integration @acme/widgets --json',
94
+ ],
92
95
  init: ['astryx init'],
93
96
  'layout expand': [`astryx layout expand 'V[g6] > C{card-callout}*4' ./src/Page.tsx`],
94
97
  'layout check': [`astryx layout check 'A[cp6] > L > LC > S[p6]' --json`],
@@ -0,0 +1,80 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Shared module-loading + conventional-file-discovery primitives.
5
+ *
6
+ * Both config loading and integration loading need to (a) import a
7
+ * user-authored module (`.ts` via jiti, `.mjs`/`.js` via native dynamic
8
+ * import) and (b) find conventional files by basename in a fixed
9
+ * load-precedence order. These helpers centralize that so the two callers stay
10
+ * in lockstep.
11
+ *
12
+ * `loadModuleWithSchema` builds on these primitives to provide the single
13
+ * load/validation boundary shared by config, integration, codemod, and
14
+ * template discovery: import the module, take its default export, and validate
15
+ * it against the expected zod schema.
16
+ */
17
+
18
+ import * as path from 'node:path';
19
+ import {pathToFileURL} from 'node:url';
20
+ import * as fs from 'node:fs';
21
+ import {createJiti} from 'jiti';
22
+ import {formatZodError} from './config-schema.mjs';
23
+
24
+ let jitiInstance;
25
+ function getJiti() {
26
+ if (!jitiInstance) {
27
+ jitiInstance = createJiti(import.meta.url);
28
+ }
29
+ return jitiInstance;
30
+ }
31
+
32
+ /**
33
+ * Import a user-authored module. `.ts` is loaded via jiti; `.mjs`/`.js` via
34
+ * native dynamic import (file:// URL). Returns the full module namespace.
35
+ * @param {string} file absolute path
36
+ * @returns {Promise<Record<string, unknown>>}
37
+ */
38
+ export async function importUserModule(file) {
39
+ if (file.endsWith('.ts')) {
40
+ return await getJiti().import(file);
41
+ }
42
+ return await import(pathToFileURL(file).href);
43
+ }
44
+
45
+ /**
46
+ * Return the conventional files (by basename, in the given precedence order)
47
+ * that exist directly in `dir`, as absolute paths. Never throws.
48
+ * @param {string} dir
49
+ * @param {string[]} basenames precedence-ordered
50
+ * @returns {string[]} absolute paths of present files, in basenames order
51
+ */
52
+ export function findPresentFiles(dir, basenames) {
53
+ return basenames
54
+ .filter(name => fs.existsSync(path.join(dir, name)))
55
+ .map(name => path.join(dir, name));
56
+ }
57
+
58
+ /**
59
+ * Import a user-authored module, take its default export, and validate it
60
+ * against a zod schema. This is the single load/validation boundary for all
61
+ * user-authored modules (config, integration, codemod, template): execute the
62
+ * module, take the default export, validate it against the expected schema.
63
+ * Throws a clear, readable error if the default export is missing or fails
64
+ * validation.
65
+ *
66
+ * @template {import('zod').ZodType} S
67
+ * @param {string} file absolute path
68
+ * @param {S} schema
69
+ * @param {{label?: string}} [opts] label used in error messages
70
+ * @returns {Promise<import('zod').infer<S>>} parsed + typed value
71
+ */
72
+ export async function loadModuleWithSchema(file, schema, {label} = {}) {
73
+ const mod = await importUserModule(file);
74
+ const exported = mod?.default;
75
+ const result = schema.safeParse(exported);
76
+ if (!result.success) {
77
+ throw new Error(formatZodError(label ?? file, result.error));
78
+ }
79
+ return result.data;
80
+ }