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