@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,308 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {pathToFileURL} from 'node:url';
|
|
7
|
+
import {Project, DEFAULT_ISSUES_URL, findConfigPath} from './project.mjs';
|
|
8
|
+
import {InMemoryConfigCache} from './config-cache.mjs';
|
|
9
|
+
import * as componentDiscovery from './component-discovery.mjs';
|
|
10
|
+
|
|
11
|
+
let tmpDir;
|
|
12
|
+
let originalCwd;
|
|
13
|
+
|
|
14
|
+
const codemodModulePath = path.resolve(
|
|
15
|
+
process.cwd(),
|
|
16
|
+
'packages/cli/src/codemod.mjs',
|
|
17
|
+
);
|
|
18
|
+
const codemodModuleUrl = pathToFileURL(codemodModulePath).href;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scaffold a consumer project (package.json + astryx.config.mjs) under a
|
|
22
|
+
* repo-local temp dir, plus an installed integration package under
|
|
23
|
+
* node_modules. The repo-local location is required so Vite permits dynamic
|
|
24
|
+
* import of the config/integration modules (it blocks /tmp).
|
|
25
|
+
*/
|
|
26
|
+
function scaffold({
|
|
27
|
+
integrations = ['@acme/widgets'],
|
|
28
|
+
issuesUrl,
|
|
29
|
+
withComponents = true,
|
|
30
|
+
withTemplates = true,
|
|
31
|
+
withCodemods = true,
|
|
32
|
+
brokenComponent = false,
|
|
33
|
+
brokenCodemod = false,
|
|
34
|
+
integrationIssuesUrl = 'https://example.com/widgets/issues',
|
|
35
|
+
} = {}) {
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(tmpDir, 'package.json'),
|
|
38
|
+
JSON.stringify({name: 'consumer'}),
|
|
39
|
+
);
|
|
40
|
+
const config = {integrations};
|
|
41
|
+
if (issuesUrl) config.issuesUrl = issuesUrl;
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
44
|
+
`export default ${JSON.stringify(config)};\n`,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
|
|
48
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(pkgDir, 'package.json'),
|
|
51
|
+
JSON.stringify({name: '@acme/widgets', version: '1.0.0'}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const manifest = {};
|
|
55
|
+
if (withComponents) manifest.components = './components';
|
|
56
|
+
if (withTemplates) manifest.templates = './templates';
|
|
57
|
+
if (withCodemods) manifest.codemods = './codemods';
|
|
58
|
+
if (integrationIssuesUrl) manifest.issuesUrl = integrationIssuesUrl;
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
61
|
+
`export default ${JSON.stringify(manifest)};\n`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (withComponents) {
|
|
65
|
+
const compDir = path.join(pkgDir, 'components');
|
|
66
|
+
fs.mkdirSync(compDir, {recursive: true});
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(compDir, 'Widget.doc.mjs'),
|
|
69
|
+
`export const docs = { name: 'Widget', usage: {description: 'A widget'} };\n`,
|
|
70
|
+
);
|
|
71
|
+
if (!brokenComponent) {
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(compDir, 'Widget.tsx'),
|
|
74
|
+
`export function Widget() { return null; }\n`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
// brokenComponent => doc with no sibling .tsx (invalid_component issue).
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (withTemplates) {
|
|
81
|
+
const tDir = path.join(pkgDir, 'templates');
|
|
82
|
+
fs.mkdirSync(tDir, {recursive: true});
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(tDir, 'hero.doc.mjs'),
|
|
85
|
+
`export default { type: 'block', name: 'Hero', description: 'A hero' };\n`,
|
|
86
|
+
);
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
path.join(tDir, 'hero.tsx'),
|
|
89
|
+
`export default function Hero() { return null; }\n`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (withCodemods) {
|
|
94
|
+
const cmDir = path.join(pkgDir, 'codemods', '0.2.0');
|
|
95
|
+
fs.mkdirSync(cmDir, {recursive: true});
|
|
96
|
+
if (brokenCodemod) {
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
path.join(cmDir, 'bad.mjs'),
|
|
99
|
+
`export default { not: 'a codemod' };\n`,
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(cmDir, 'drop-foo.mjs'),
|
|
104
|
+
`import {createCodemod} from ${JSON.stringify(codemodModuleUrl)};\n` +
|
|
105
|
+
`export default createCodemod({ title: 'Drop foo', transform: (file) => file.source });\n`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return pkgDir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
originalCwd = process.cwd();
|
|
115
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-project-test-'));
|
|
116
|
+
process.chdir(tmpDir);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
process.chdir(originalCwd);
|
|
121
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
122
|
+
vi.restoreAllMocks();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('Project.load', () => {
|
|
126
|
+
it('returns an empty config + no integrations when no config file exists', async () => {
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(tmpDir, 'package.json'),
|
|
129
|
+
JSON.stringify({name: 'consumer'}),
|
|
130
|
+
);
|
|
131
|
+
const project = await Project.load(tmpDir);
|
|
132
|
+
expect(project.config).toEqual({integrations: []});
|
|
133
|
+
expect(project.integrations).toEqual([]);
|
|
134
|
+
expect(project.loadedIntegrations).toEqual([]);
|
|
135
|
+
expect(project.cwd).toBe(tmpDir);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('exposes the validated config surface and loaded integrations', async () => {
|
|
139
|
+
scaffold({issuesUrl: 'https://example.com/issues'});
|
|
140
|
+
const project = await Project.load(tmpDir);
|
|
141
|
+
expect(project.config.issuesUrl).toBe('https://example.com/issues');
|
|
142
|
+
expect(project.integrations).toEqual(['@acme/widgets']);
|
|
143
|
+
expect(project.loadedIntegrations).toHaveLength(1);
|
|
144
|
+
expect(project.loadedIntegrations[0].name).toBe('@acme/widgets');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('rejects an invalid config shape (strict validation)', async () => {
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
path.join(tmpDir, 'package.json'),
|
|
150
|
+
JSON.stringify({name: 'consumer'}),
|
|
151
|
+
);
|
|
152
|
+
fs.writeFileSync(
|
|
153
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
154
|
+
`export default { integrations: [42] };\n`,
|
|
155
|
+
);
|
|
156
|
+
await expect(Project.load(tmpDir)).rejects.toThrow(/integrations/);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('findConfigPath', () => {
|
|
161
|
+
it('rejects multiple config files at the same root', () => {
|
|
162
|
+
fs.writeFileSync(
|
|
163
|
+
path.join(tmpDir, 'package.json'),
|
|
164
|
+
JSON.stringify({name: 'consumer'}),
|
|
165
|
+
);
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
168
|
+
`export default {};\n`,
|
|
169
|
+
);
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(tmpDir, 'astryx.config.js'),
|
|
172
|
+
`module.exports = {};\n`,
|
|
173
|
+
);
|
|
174
|
+
expect(() => findConfigPath(tmpDir)).toThrow(/Multiple Astryx config files/);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('Project discovery', () => {
|
|
179
|
+
it('components() returns integration ownership records', async () => {
|
|
180
|
+
scaffold();
|
|
181
|
+
const project = await Project.load(tmpDir);
|
|
182
|
+
const comps = await project.components();
|
|
183
|
+
const widget = comps.find(
|
|
184
|
+
c => c.name === 'Widget' && c.package === '@acme/widgets',
|
|
185
|
+
);
|
|
186
|
+
expect(widget).toBeTruthy();
|
|
187
|
+
expect(widget.sourcePath).toMatch(/Widget\.tsx$/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('templates() returns integration templates type-tagged', async () => {
|
|
191
|
+
scaffold();
|
|
192
|
+
const project = await Project.load(tmpDir);
|
|
193
|
+
const templates = await project.templates();
|
|
194
|
+
const hero = templates.find(t => t.package === '@acme/widgets');
|
|
195
|
+
expect(hero).toBeTruthy();
|
|
196
|
+
expect(hero.type).toBe('block');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('codemods() returns core registry groups + integration groups', async () => {
|
|
200
|
+
scaffold();
|
|
201
|
+
const project = await Project.load(tmpDir);
|
|
202
|
+
const {core, integration} = await project.codemods('0.1.0', '0.2.0');
|
|
203
|
+
expect(Array.isArray(core)).toBe(true);
|
|
204
|
+
expect(integration).toHaveLength(1);
|
|
205
|
+
expect(integration[0].version).toBe('0.2.0');
|
|
206
|
+
expect(integration[0].codemods[0].id).toBe('drop-foo');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('memoizes components() — the second call does not re-walk', async () => {
|
|
210
|
+
scaffold();
|
|
211
|
+
const project = await Project.load(tmpDir);
|
|
212
|
+
const spy = vi.spyOn(componentDiscovery, 'discoverIntegrationComponents');
|
|
213
|
+
const first = await project.components();
|
|
214
|
+
const callsAfterFirst = spy.mock.calls.length;
|
|
215
|
+
const second = await project.components();
|
|
216
|
+
// Same identity (cached value) and no additional discovery walk.
|
|
217
|
+
expect(second).toBe(first);
|
|
218
|
+
expect(spy.mock.calls.length).toBe(callsAfterFirst);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Project issues (skip + warn)', () => {
|
|
223
|
+
it('collects issues for a broken integration and skips its contributions', async () => {
|
|
224
|
+
scaffold({brokenComponent: true});
|
|
225
|
+
const project = await Project.load(tmpDir);
|
|
226
|
+
const comps = await project.components();
|
|
227
|
+
// Widget is skipped because its source is missing.
|
|
228
|
+
expect(comps.find(c => c.package === '@acme/widgets')).toBeUndefined();
|
|
229
|
+
const issues = await project.issues();
|
|
230
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
231
|
+
expect(issues.some(i => i.package === '@acme/widgets')).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('does not throw when an integration codemod is broken', async () => {
|
|
235
|
+
scaffold({brokenCodemod: true});
|
|
236
|
+
const project = await Project.load(tmpDir);
|
|
237
|
+
const {integration} = await project.codemods('0.1.0', '0.2.0');
|
|
238
|
+
// Broken integration is skipped, not thrown.
|
|
239
|
+
expect(integration).toHaveLength(0);
|
|
240
|
+
const issues = await project.issues();
|
|
241
|
+
expect(issues.some(i => i.code === 'invalid_codemod')).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('issues() dedupes and is complete on demand', async () => {
|
|
245
|
+
scaffold({brokenComponent: true});
|
|
246
|
+
const project = await Project.load(tmpDir);
|
|
247
|
+
// Call a discovery method first (collects some), then issues().
|
|
248
|
+
await project.components();
|
|
249
|
+
const a = await project.issues();
|
|
250
|
+
const b = await project.issues();
|
|
251
|
+
expect(a).toEqual(b);
|
|
252
|
+
// No duplicate (package, code, message) tuples.
|
|
253
|
+
const seen = new Set(a.map(i => `${i.package}\u0000${i.code}\u0000${i.message}`));
|
|
254
|
+
expect(seen.size).toBe(a.length);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('issues() validates integrations not yet visited by a discovery call', async () => {
|
|
258
|
+
scaffold({brokenComponent: true});
|
|
259
|
+
const project = await Project.load(tmpDir);
|
|
260
|
+
// No discovery call at all — issues() must still surface the broken one.
|
|
261
|
+
const issues = await project.issues();
|
|
262
|
+
expect(issues.some(i => i.package === '@acme/widgets')).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('Project.issuesUrl routing', () => {
|
|
267
|
+
it('routes core to config.issuesUrl, falling back to the default', async () => {
|
|
268
|
+
scaffold({issuesUrl: 'https://example.com/core'});
|
|
269
|
+
const project = await Project.load(tmpDir);
|
|
270
|
+
expect(project.issuesUrl()).toBe('https://example.com/core');
|
|
271
|
+
expect(project.issuesUrl({package: '@astryxdesign/core'})).toBe(
|
|
272
|
+
'https://example.com/core',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('falls back to the default core URL when config has none', async () => {
|
|
277
|
+
scaffold();
|
|
278
|
+
const project = await Project.load(tmpDir);
|
|
279
|
+
expect(project.issuesUrl()).toBe(DEFAULT_ISSUES_URL);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('routes an integration package to its manifest issuesUrl', async () => {
|
|
283
|
+
scaffold({integrationIssuesUrl: 'https://example.com/widgets/issues'});
|
|
284
|
+
const project = await Project.load(tmpDir);
|
|
285
|
+
expect(project.issuesUrl({package: '@acme/widgets'})).toBe(
|
|
286
|
+
'https://example.com/widgets/issues',
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('returns undefined for an integration that ships no issuesUrl', async () => {
|
|
291
|
+
scaffold({integrationIssuesUrl: null});
|
|
292
|
+
const project = await Project.load(tmpDir);
|
|
293
|
+
expect(project.issuesUrl({package: '@acme/widgets'})).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('Project caching', () => {
|
|
298
|
+
it('reuses a shared cache across instances for the same config bytes', async () => {
|
|
299
|
+
scaffold();
|
|
300
|
+
const cache = new InMemoryConfigCache();
|
|
301
|
+
const p1 = await Project.load(tmpDir, {cache});
|
|
302
|
+
const comps1 = await p1.components();
|
|
303
|
+
const p2 = await Project.load(tmpDir, {cache});
|
|
304
|
+
const comps2 = await p2.components();
|
|
305
|
+
// Same cached value across Project instances (same key path).
|
|
306
|
+
expect(comps2).toBe(comps1);
|
|
307
|
+
});
|
|
308
|
+
});
|
package/src/lib/site.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file site.mjs — the canonical Astryx site origin for CLI code.
|
|
5
|
+
*
|
|
6
|
+
* The published Astryx site is the single source of truth for docs, the blog,
|
|
7
|
+
* and the MCP endpoint. Anything in the CLI that needs to reach the site reads
|
|
8
|
+
* this constant, so the origin is defined in exactly one place. It is
|
|
9
|
+
* intentionally not user-configurable: the CLI should never be pointed at an
|
|
10
|
+
* arbitrary host.
|
|
11
|
+
*
|
|
12
|
+
* (The docsite has its own SITE_URL in apps/docsite; the CLI is a separately
|
|
13
|
+
* published package and can't import from the app, so the origin is mirrored
|
|
14
|
+
* here for CLI use.)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const SITE_URL = 'https://astryx.atmeta.com';
|
|
18
|
+
|
|
19
|
+
/** The site's origin (scheme + host), used for same-origin checks. */
|
|
20
|
+
export const SITE_ORIGIN = new URL(SITE_URL).origin;
|
package/src/template.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Static template authoring API.
|
|
5
|
+
*
|
|
6
|
+
* Helpers for authoring Astryx page/block template docs. Like
|
|
7
|
+
* `createConfig`/`createIntegration`, these are tiny runtime identity helpers
|
|
8
|
+
* whose real value is the exported TypeScript surface from
|
|
9
|
+
* `@astryxdesign/cli/template`. They inject the discriminant `type` so a
|
|
10
|
+
* discovered doc always knows whether it is a page or a block. They do NOT
|
|
11
|
+
* validate — validation happens at the load boundary, where integration
|
|
12
|
+
* template discovery runs the module's default export through
|
|
13
|
+
* {@link TemplateEnvelopeSchema} (see `loadModuleWithSchema`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {z} from 'zod';
|
|
17
|
+
|
|
18
|
+
const PreviewSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
image: z.string().optional(),
|
|
21
|
+
aspectRatio: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared authored-template shape. `type` is injected by the create* helpers,
|
|
27
|
+
* so authors never write it. Inline source/sourceFile are intentionally NOT
|
|
28
|
+
* part of v1 — a template's source is the required same-stem sibling file.
|
|
29
|
+
* Exported so integration template discovery can validate the stamped result.
|
|
30
|
+
*/
|
|
31
|
+
export const BaseTemplateSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
name: z.string().min(1, 'name is required'),
|
|
34
|
+
description: z.string().min(1, 'description is required'),
|
|
35
|
+
category: z.string().optional(),
|
|
36
|
+
componentsUsed: z.array(z.string()).optional(),
|
|
37
|
+
preview: PreviewSchema.optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The metadata envelope integration template discovery validates: a stamped
|
|
43
|
+
* template doc. This is the LOAD-boundary contract — a hand-written plain
|
|
44
|
+
* object that matches this shape is accepted (discovery does not check "was it
|
|
45
|
+
* made by the factory", only the shape).
|
|
46
|
+
*/
|
|
47
|
+
export const TemplateEnvelopeSchema = BaseTemplateSchema.extend({
|
|
48
|
+
type: z.enum(['page', 'block']),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Author an Astryx page template doc. Stamp-only: returns the def with
|
|
53
|
+
* `type: 'page'` injected. Validation happens at the load boundary.
|
|
54
|
+
*
|
|
55
|
+
* @template {import('./types/template-api').AstryxPageTemplateInput} T
|
|
56
|
+
* @param {T} def
|
|
57
|
+
* @returns {T & {type: 'page'}}
|
|
58
|
+
*/
|
|
59
|
+
export function createPageTemplate(def) {
|
|
60
|
+
return /** @type {T & {type: 'page'}} */ ({...def, type: 'page'});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Author an Astryx block template doc. Stamp-only: returns the def with
|
|
65
|
+
* `type: 'block'` injected. Validation happens at the load boundary.
|
|
66
|
+
*
|
|
67
|
+
* @template {import('./types/template-api').AstryxBlockTemplateInput} T
|
|
68
|
+
* @param {T} def
|
|
69
|
+
* @returns {T & {type: 'block'}}
|
|
70
|
+
*/
|
|
71
|
+
export function createBlockTemplate(def) {
|
|
72
|
+
return /** @type {T & {type: 'block'}} */ ({...def, type: 'block'});
|
|
73
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
createPageTemplate,
|
|
6
|
+
createBlockTemplate,
|
|
7
|
+
TemplateEnvelopeSchema,
|
|
8
|
+
} from './template.mjs';
|
|
9
|
+
|
|
10
|
+
// createPageTemplate/createBlockTemplate are now stamp-only: they inject the
|
|
11
|
+
// `type` discriminant and return the def otherwise unchanged, with NO runtime
|
|
12
|
+
// validation. Validation happens at the LOAD boundary (integration template
|
|
13
|
+
// discovery runs the default export through TemplateEnvelopeSchema), so the
|
|
14
|
+
// rejection cases that used to assert factory throws now assert the envelope
|
|
15
|
+
// schema rejects the same shapes.
|
|
16
|
+
|
|
17
|
+
describe('createPageTemplate (stamp-only)', () => {
|
|
18
|
+
it('returns the def with type "page" injected', () => {
|
|
19
|
+
const t = createPageTemplate({
|
|
20
|
+
name: 'Landing',
|
|
21
|
+
description: 'A landing page.',
|
|
22
|
+
});
|
|
23
|
+
expect(t).toEqual({
|
|
24
|
+
name: 'Landing',
|
|
25
|
+
description: 'A landing page.',
|
|
26
|
+
type: 'page',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('preserves optional fields', () => {
|
|
31
|
+
const t = createPageTemplate({
|
|
32
|
+
name: 'Landing',
|
|
33
|
+
description: 'A landing page.',
|
|
34
|
+
category: 'Marketing',
|
|
35
|
+
componentsUsed: ['Button', 'Card'],
|
|
36
|
+
preview: {image: './preview.png', aspectRatio: '16 / 9'},
|
|
37
|
+
});
|
|
38
|
+
expect(t.category).toBe('Marketing');
|
|
39
|
+
expect(t.componentsUsed).toEqual(['Button', 'Card']);
|
|
40
|
+
expect(t.preview).toEqual({image: './preview.png', aspectRatio: '16 / 9'});
|
|
41
|
+
expect(t.type).toBe('page');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('does NOT validate — returns an invalid def stamped, unchanged', () => {
|
|
45
|
+
const t = createPageTemplate({description: 'x'});
|
|
46
|
+
expect(t.type).toBe('page');
|
|
47
|
+
expect(t.name).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('createBlockTemplate (stamp-only)', () => {
|
|
52
|
+
it('returns the def with type "block" injected', () => {
|
|
53
|
+
const t = createBlockTemplate({
|
|
54
|
+
name: 'Hero',
|
|
55
|
+
description: 'A hero block.',
|
|
56
|
+
});
|
|
57
|
+
expect(t.type).toBe('block');
|
|
58
|
+
expect(t.name).toBe('Hero');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('TemplateEnvelopeSchema (load-boundary validation)', () => {
|
|
63
|
+
it('accepts a stamped page template', () => {
|
|
64
|
+
const parsed = TemplateEnvelopeSchema.parse(
|
|
65
|
+
createPageTemplate({name: 'Landing', description: 'A landing page.'}),
|
|
66
|
+
);
|
|
67
|
+
expect(parsed.type).toBe('page');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('accepts a PLAIN OBJECT envelope (no factory required)', () => {
|
|
71
|
+
const parsed = TemplateEnvelopeSchema.parse({
|
|
72
|
+
type: 'block',
|
|
73
|
+
name: 'Hero',
|
|
74
|
+
description: 'A hero block.',
|
|
75
|
+
});
|
|
76
|
+
expect(parsed.name).toBe('Hero');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects a missing name', () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
TemplateEnvelopeSchema.parse({type: 'page', description: 'x'}),
|
|
82
|
+
).toThrow(/name/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects a missing description', () => {
|
|
86
|
+
expect(() =>
|
|
87
|
+
TemplateEnvelopeSchema.parse({type: 'page', name: 'x'}),
|
|
88
|
+
).toThrow(/description/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects an empty-string name', () => {
|
|
92
|
+
expect(() =>
|
|
93
|
+
TemplateEnvelopeSchema.parse({type: 'page', name: '', description: 'x'}),
|
|
94
|
+
).toThrow(/name/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects a missing/invalid type', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
TemplateEnvelopeSchema.parse({name: 'x', description: 'y'}),
|
|
100
|
+
).toThrow();
|
|
101
|
+
expect(() =>
|
|
102
|
+
TemplateEnvelopeSchema.parse({type: 'bogus', name: 'x', description: 'y'}),
|
|
103
|
+
).toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects unknown keys (strict)', () => {
|
|
107
|
+
expect(() =>
|
|
108
|
+
TemplateEnvelopeSchema.parse({
|
|
109
|
+
type: 'page',
|
|
110
|
+
name: 'x',
|
|
111
|
+
description: 'y',
|
|
112
|
+
source: './x.tsx',
|
|
113
|
+
}),
|
|
114
|
+
).toThrow();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('rejects inline sourceFile (not supported in v1)', () => {
|
|
118
|
+
expect(() =>
|
|
119
|
+
TemplateEnvelopeSchema.parse({
|
|
120
|
+
type: 'page',
|
|
121
|
+
name: 'x',
|
|
122
|
+
description: 'y',
|
|
123
|
+
sourceFile: './x.tsx',
|
|
124
|
+
}),
|
|
125
|
+
).toThrow();
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/types/api.d.ts
CHANGED
|
@@ -33,7 +33,6 @@ import type {
|
|
|
33
33
|
TemplateShowResponse,
|
|
34
34
|
TemplateSkeletonResponse,
|
|
35
35
|
TemplateCopyResponse,
|
|
36
|
-
TemplateGetResponse,
|
|
37
36
|
} from './template';
|
|
38
37
|
import type {
|
|
39
38
|
HookListResponse,
|
|
@@ -138,6 +137,8 @@ export interface TemplateOptions {
|
|
|
138
137
|
show?: boolean;
|
|
139
138
|
/** Filter templates by kind: 'page' or 'block'. Only applies to list views. */
|
|
140
139
|
type?: 'page' | 'block';
|
|
140
|
+
/** Narrow to templates from a specific package (id-only lookups across packages are ambiguous). */
|
|
141
|
+
package?: string;
|
|
141
142
|
targetPath?: string;
|
|
142
143
|
cwd?: string;
|
|
143
144
|
}
|
|
@@ -153,11 +154,6 @@ export declare function template(
|
|
|
153
154
|
options?: TemplateOptions,
|
|
154
155
|
): Promise<TemplateResult>;
|
|
155
156
|
|
|
156
|
-
export declare function getTemplateById(
|
|
157
|
-
id: string,
|
|
158
|
-
options?: {cwd?: string},
|
|
159
|
-
): Promise<TemplateGetResponse>;
|
|
160
|
-
|
|
161
157
|
// ── Hook ─────────────────────────────────────────────────────────────
|
|
162
158
|
|
|
163
159
|
export interface HookOptions {
|
package/src/types/base.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ import type {
|
|
|
32
32
|
TemplateShowResponse,
|
|
33
33
|
TemplateSkeletonResponse,
|
|
34
34
|
TemplateCopyResponse,
|
|
35
|
-
TemplateGetResponse,
|
|
36
35
|
} from './template';
|
|
37
36
|
import type {
|
|
38
37
|
HookListResponse,
|
|
@@ -44,14 +43,11 @@ import type {
|
|
|
44
43
|
import type {SwizzleListResponse, SwizzleCopyResponse} from './swizzle';
|
|
45
44
|
import type {ThemeBuildResponse} from './theme';
|
|
46
45
|
import type {UpgradeListResponse, UpgradeRunResponse} from './upgrade';
|
|
47
|
-
import type {
|
|
48
|
-
GapReportCategoriesResponse,
|
|
49
|
-
GapReportFileResponse,
|
|
50
|
-
} from './gap-report';
|
|
51
46
|
import type {SearchResponse} from './search';
|
|
52
47
|
import type {ErrorCode} from './error-codes';
|
|
53
48
|
import type {ManifestResponse} from './manifest';
|
|
54
49
|
import type {DoctorResponse} from './doctor';
|
|
50
|
+
import type {ValidateIntegrationResponse} from './validate-integration';
|
|
55
51
|
|
|
56
52
|
/**
|
|
57
53
|
* Structured error. Check `'error' in result` to discriminate.
|
|
@@ -93,7 +89,6 @@ export type CLIAnyResponse =
|
|
|
93
89
|
| TemplateShowResponse
|
|
94
90
|
| TemplateSkeletonResponse
|
|
95
91
|
| TemplateCopyResponse
|
|
96
|
-
| TemplateGetResponse
|
|
97
92
|
| HookListResponse
|
|
98
93
|
| HookBriefResponse
|
|
99
94
|
| HookFullResponse
|
|
@@ -104,11 +99,10 @@ export type CLIAnyResponse =
|
|
|
104
99
|
| ThemeBuildResponse
|
|
105
100
|
| UpgradeListResponse
|
|
106
101
|
| UpgradeRunResponse
|
|
107
|
-
| GapReportCategoriesResponse
|
|
108
|
-
| GapReportFileResponse
|
|
109
102
|
| SearchResponse
|
|
110
103
|
| ManifestResponse
|
|
111
|
-
| DoctorResponse
|
|
104
|
+
| DoctorResponse
|
|
105
|
+
| ValidateIntegrationResponse;
|
|
112
106
|
|
|
113
107
|
/** Union of all type discriminator string literals. */
|
|
114
108
|
export type CLIResponseType = CLIAnyResponse['type'];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public type surface for the file-based codemod-authoring API exported from
|
|
5
|
+
* `@astryxdesign/cli/codemod`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A single source file presented to a codemod's transform. */
|
|
9
|
+
export interface AstryxCodemodFile {
|
|
10
|
+
/** Absolute path to the file being transformed. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** The current source contents of the file. */
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Helpers and context passed to a codemod's transform as the second argument. */
|
|
17
|
+
export interface AstryxCodemodApi {
|
|
18
|
+
/** A jscodeshift instance configured with a parser for the file. */
|
|
19
|
+
jscodeshift: unknown;
|
|
20
|
+
/** Report a statistic (no-op-friendly; provided for jscodeshift parity). */
|
|
21
|
+
stats: (...args: unknown[]) => void;
|
|
22
|
+
/** Report progress (no-op-friendly; provided for jscodeshift parity). */
|
|
23
|
+
report: (...args: unknown[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A codemod's transform. Return the new source to rewrite the file, or
|
|
28
|
+
* `null`/`undefined` to leave the file unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export type AstryxCodemodTransform = (
|
|
31
|
+
file: AstryxCodemodFile,
|
|
32
|
+
api: AstryxCodemodApi,
|
|
33
|
+
) => string | null | undefined;
|
|
34
|
+
|
|
35
|
+
/** Definition accepted by {@link createCodemod}. */
|
|
36
|
+
export interface AstryxCodemodDef {
|
|
37
|
+
/** Short, human-readable title shown in upgrade output. */
|
|
38
|
+
title: string;
|
|
39
|
+
/** Optional longer description. */
|
|
40
|
+
description?: string;
|
|
41
|
+
/** When true, the codemod runs only when explicitly requested. */
|
|
42
|
+
isOptional?: boolean;
|
|
43
|
+
/** File extensions this codemod applies to (e.g. ['.tsx', '.ts']). */
|
|
44
|
+
fileExtensions?: string[];
|
|
45
|
+
/** The transform function. */
|
|
46
|
+
transform: AstryxCodemodTransform;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Result of {@link createCodemod}. */
|
|
50
|
+
export interface AstryxCodemod extends AstryxCodemodDef {
|
|
51
|
+
isOptional: boolean;
|
|
52
|
+
type: 'code';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Definition accepted by {@link createConfigCodemod}. */
|
|
56
|
+
export interface AstryxConfigCodemodDef {
|
|
57
|
+
/** Short, human-readable title shown in upgrade output. */
|
|
58
|
+
title: string;
|
|
59
|
+
/** Optional longer description. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/** When true, the codemod runs only when explicitly requested. */
|
|
62
|
+
isOptional?: boolean;
|
|
63
|
+
/** The transform function applied to the astryx.config.* file. */
|
|
64
|
+
transform: AstryxCodemodTransform;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Result of {@link createConfigCodemod}. */
|
|
68
|
+
export interface AstryxConfigCodemod extends AstryxConfigCodemodDef {
|
|
69
|
+
isOptional: boolean;
|
|
70
|
+
type: 'config';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Define a file-transforming codemod. */
|
|
74
|
+
export declare function createCodemod<T extends AstryxCodemodDef>(
|
|
75
|
+
def: T,
|
|
76
|
+
): AstryxCodemod;
|
|
77
|
+
|
|
78
|
+
/** Define a codemod that targets the consumer's astryx.config.* file. */
|
|
79
|
+
export declare function createConfigCodemod<T extends AstryxConfigCodemodDef>(
|
|
80
|
+
def: T,
|
|
81
|
+
): AstryxConfigCodemod;
|