@astryxdesign/cli 0.1.0 → 0.1.1-canary.080d887
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 +68 -0
- package/README.md +117 -75
- package/bin/astryx.mjs +22 -7
- package/docs/getting-started.doc.mjs +11 -11
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +3 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +20 -9
- package/src/api/discover.mjs +78 -26
- package/src/api/doctor.mjs +3 -3
- package/src/api/layout.mjs +301 -0
- package/src/api/layout.test.mjs +238 -0
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +193 -51
- package/src/api/template.test.mjs +2 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +119 -66
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +87 -31
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +43 -9
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +292 -177
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/index.mjs +5 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +45 -6
- package/src/lib/config.test.mjs +91 -0
- package/src/lib/error-codes.mjs +11 -0
- package/src/lib/integrations.mjs +155 -0
- package/src/lib/integrations.test.mjs +154 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +6 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +91 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/types/config.d.ts +99 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/github.mjs +12 -27
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.mjs +1 -1
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +42 -58
- package/templates/pages/documentation-design/page.tsx +82 -60
- package/templates/pages/documentation-technical/page.tsx +101 -60
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +13 -17
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +188 -264
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -67
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file build command — the page-building assistant.
|
|
5
|
+
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
* astryx build → the PLAYBOOK: how to build a page with Astryx
|
|
8
|
+
* astryx build "<what>" → a COMPOSITION KIT for what you're building:
|
|
9
|
+
* the closest page template (scaffold or layout
|
|
10
|
+
* reference), the blocks that cover parts, and
|
|
11
|
+
* the components to fill gaps, plus a one-line
|
|
12
|
+
* "Compose:" suggestion.
|
|
13
|
+
*
|
|
14
|
+
* `build` is the opinionated "assemble a page" verb. For a neutral lookup across
|
|
15
|
+
* the whole CLI, use `astryx search <query>` instead.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
19
|
+
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
20
|
+
import {cliError} from '../lib/cli-error.mjs';
|
|
21
|
+
import {search as searchApi} from '../api/search.mjs';
|
|
22
|
+
|
|
23
|
+
/** A page scoring at/above this is confident enough to call a direct match. */
|
|
24
|
+
const PAGE_DIRECT = 95;
|
|
25
|
+
/** Below this a page is too weak to even offer as a layout reference. */
|
|
26
|
+
const PAGE_FLOOR = 50;
|
|
27
|
+
/** Below this a block/domain-component match is incidental noise, not surfaced. */
|
|
28
|
+
const DOMAIN_FLOOR = 55;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Always-surfaced primitives. Every page needs a shell + layout/typography/
|
|
32
|
+
* action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
|
|
33
|
+
* so search alone never returns them. We list them unconditionally so an agent
|
|
34
|
+
* composing from scratch has the whole kit (esp. off-template).
|
|
35
|
+
*/
|
|
36
|
+
const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
|
|
37
|
+
const FOUNDATION = [
|
|
38
|
+
'VStack', 'HStack', 'Grid', 'StackItem', 'Card', 'Section',
|
|
39
|
+
'Text', 'Heading', 'Button', 'Icon', 'Badge', 'Divider',
|
|
40
|
+
];
|
|
41
|
+
const ALWAYS = new Set([...FRAME, ...FOUNDATION]);
|
|
42
|
+
|
|
43
|
+
/** Print the build playbook (shown when `build` is run with no query). */
|
|
44
|
+
function printPlaybook(run) {
|
|
45
|
+
const lines = [
|
|
46
|
+
'',
|
|
47
|
+
'How to build a page with Astryx',
|
|
48
|
+
'',
|
|
49
|
+
"1. Find a starting point for what you're building:",
|
|
50
|
+
` ${run} astryx build "<what you're building>"`,
|
|
51
|
+
' → returns the closest [page] template, the [block]s that cover parts,',
|
|
52
|
+
' and the [component]s to fill the gaps, with a "Compose:" suggestion.',
|
|
53
|
+
'',
|
|
54
|
+
'2. If a [page] template matches → scaffold it and adapt:',
|
|
55
|
+
` ${run} astryx template <name> [path]`,
|
|
56
|
+
'',
|
|
57
|
+
'3. If nothing matches exactly → compose:',
|
|
58
|
+
` ${run} astryx template <name> --skeleton # study a close page's layout`,
|
|
59
|
+
` ${run} astryx template <BlockName> # drop in each block from the kit`,
|
|
60
|
+
` ${run} astryx component <Name> # fill remaining gaps (read props)`,
|
|
61
|
+
'',
|
|
62
|
+
'4. Rules (keep it on-system):',
|
|
63
|
+
' - No <div>/raw HTML for layout — use VStack/HStack/Grid/Stack/Card etc.',
|
|
64
|
+
' - No style={{}} — use component props; design tokens via `astryx docs tokens`.',
|
|
65
|
+
' - Wrap the app in <Theme theme={...}> and import core reset.css + astryx.css.',
|
|
66
|
+
'',
|
|
67
|
+
`Tip: \`${run} astryx build "<idea>"\` is the fastest way in. For a neutral`,
|
|
68
|
+
`lookup of any component/doc/template, use \`${run} astryx search <query>\`.`,
|
|
69
|
+
'',
|
|
70
|
+
];
|
|
71
|
+
for (const l of lines) humanLog(l);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function registerBuild(program) {
|
|
75
|
+
program
|
|
76
|
+
.command('build [query]')
|
|
77
|
+
.description('Build a page: composition kit for an idea, or the workflow playbook (no args)')
|
|
78
|
+
.option('--type <domain>', 'Filter the kit to one domain (component|hook|template)')
|
|
79
|
+
.option('--limit <n>', 'Max candidates to draw from (default 60)')
|
|
80
|
+
.option('--detail', 'Verbose output (include import paths and match reason)')
|
|
81
|
+
.action(async (query, options) => {
|
|
82
|
+
const run = getRunPrefix();
|
|
83
|
+
const json = program.opts().json || false;
|
|
84
|
+
|
|
85
|
+
// No query → print the playbook (the "how to build" skill).
|
|
86
|
+
if (!query || !String(query).trim()) {
|
|
87
|
+
if (json) return jsonOut('build.help', {playbook: true});
|
|
88
|
+
printPlaybook(run);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default to a deep pool so each role section has candidates after grouping.
|
|
93
|
+
let limit = 60;
|
|
94
|
+
if (options.limit != null) {
|
|
95
|
+
const parsed = Number.parseInt(options.limit, 10);
|
|
96
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
97
|
+
cliError(`Invalid --limit value "${options.limit}". Must be a positive integer.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
limit = parsed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = await searchApi(query, {cwd: process.cwd(), type: options.type, limit});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
cliError(e.message, {suggestions: e.suggestions});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (json) return jsonOut(result.type, result.data);
|
|
112
|
+
|
|
113
|
+
const {query: q, results} = result.data;
|
|
114
|
+
|
|
115
|
+
if (results.length === 0) {
|
|
116
|
+
humanLog('');
|
|
117
|
+
humanLog(`No matches for "${q}".`);
|
|
118
|
+
humanLog(`Try a broader term, or browse: ${run} astryx component --list`);
|
|
119
|
+
humanLog('');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Group results by role (the build kit) ──────────────────────
|
|
124
|
+
const pages = results
|
|
125
|
+
.filter(r => r.domain === 'template' && r.kind !== 'block' && r.score >= PAGE_FLOOR)
|
|
126
|
+
.slice(0, 3);
|
|
127
|
+
const blocks = results
|
|
128
|
+
.filter(r => r.domain === 'template' && r.kind === 'block' && r.score >= DOMAIN_FLOOR)
|
|
129
|
+
.slice(0, 5);
|
|
130
|
+
// Idea-specific atoms = matched components/hooks MINUS the always-on kit.
|
|
131
|
+
const domain = results
|
|
132
|
+
.filter(r => (r.domain === 'component' || r.domain === 'hook') && r.score >= DOMAIN_FLOOR && !ALWAYS.has(r.name))
|
|
133
|
+
.slice(0, 6);
|
|
134
|
+
const directMatch = pages.length > 0 && pages[0].score >= PAGE_DIRECT;
|
|
135
|
+
|
|
136
|
+
const printItem = (r, label) => {
|
|
137
|
+
const display = r.domain === 'template' && r.displayName ? r.displayName : r.name;
|
|
138
|
+
humanLog('');
|
|
139
|
+
humanLog(` [${label}] ${display}`);
|
|
140
|
+
if (r.description) humanLog(` ${r.description}`);
|
|
141
|
+
humanLog(` → ${run} ${r.command}`);
|
|
142
|
+
if (options.detail) {
|
|
143
|
+
if (r.import) humanLog(` import: ${r.import}`);
|
|
144
|
+
humanLog(` match: ${r.reason} (score ${r.score})`);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
humanLog('');
|
|
149
|
+
humanLog(`Building "${q}":`);
|
|
150
|
+
|
|
151
|
+
// START — the single recommended path.
|
|
152
|
+
humanLog('');
|
|
153
|
+
if (directMatch) {
|
|
154
|
+
humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} astryx template ${pages[0].name} ./src/App.tsx`);
|
|
155
|
+
} else if (pages.length) {
|
|
156
|
+
humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run} astryx template ${pages[0].name} --skeleton) and compose the pieces below.`);
|
|
157
|
+
} else {
|
|
158
|
+
humanLog(`START → No page template fits. Frame with AppShell and compose the blocks + components below.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// PAGE
|
|
162
|
+
if (pages.length) {
|
|
163
|
+
humanLog('');
|
|
164
|
+
humanLog(directMatch ? 'PAGE TEMPLATE — direct match:' : 'CLOSEST PAGE TEMPLATES — layout reference:');
|
|
165
|
+
pages.forEach(p => printItem(p, directMatch ? 'page' : 'closest'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// FRAME — always (the page shell).
|
|
169
|
+
humanLog('');
|
|
170
|
+
humanLog(`FRAME — page shell (always): ${FRAME.join(', ')}`);
|
|
171
|
+
humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run} astryx component AppShell`);
|
|
172
|
+
|
|
173
|
+
// BLOCKS — idea-specific composed patterns.
|
|
174
|
+
if (blocks.length) {
|
|
175
|
+
humanLog('');
|
|
176
|
+
humanLog('BLOCKS — drop-in patterns that cover parts of it:');
|
|
177
|
+
blocks.forEach(b => printItem(b, 'block'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// DOMAIN COMPONENTS — idea-specific atoms.
|
|
181
|
+
if (domain.length) {
|
|
182
|
+
humanLog('');
|
|
183
|
+
humanLog('DOMAIN COMPONENTS — specific to this idea:');
|
|
184
|
+
domain.forEach(c => printItem(c, c.domain === 'hook' ? 'hook' : 'component'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// FOUNDATION — always (layout/typography/actions).
|
|
188
|
+
humanLog('');
|
|
189
|
+
humanLog(`FOUNDATION — always available (layout/text/actions): ${FOUNDATION.join(' ')}`);
|
|
190
|
+
|
|
191
|
+
// SETUP — so it renders / stays on-system.
|
|
192
|
+
humanLog('');
|
|
193
|
+
humanLog('SETUP — import "@astryxdesign/core/reset.css" + "astryx.css". No <div>/style for layout — use Stack/Grid + tokens.');
|
|
194
|
+
humanLog('');
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -10,7 +10,7 @@ import {registerDocs} from './docs.mjs';
|
|
|
10
10
|
let tmpDir;
|
|
11
11
|
|
|
12
12
|
beforeEach(() => {
|
|
13
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-docs-test-'));
|
|
14
14
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
15
15
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
16
16
|
});
|
|
@@ -26,7 +26,7 @@ let logCalls;
|
|
|
26
26
|
let exitCode;
|
|
27
27
|
|
|
28
28
|
beforeEach(() => {
|
|
29
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
29
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-doctor-test-'));
|
|
30
30
|
logCalls = [];
|
|
31
31
|
exitCode = undefined;
|
|
32
32
|
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
@@ -132,7 +132,7 @@ export function registerGapReport(program) {
|
|
|
132
132
|
.action(async () => {
|
|
133
133
|
p.intro('Gap report setup');
|
|
134
134
|
|
|
135
|
-
const config = loadGapReportConfig();
|
|
135
|
+
const config = await loadGapReportConfig();
|
|
136
136
|
const currentMode = !config.enabled
|
|
137
137
|
? 'disabled'
|
|
138
138
|
: config.command
|
|
@@ -252,9 +252,14 @@ export function registerGapReport(program) {
|
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
const config = loadGapReportConfig();
|
|
255
|
+
const config = await loadGapReportConfig();
|
|
256
256
|
if (!config.enabled) {
|
|
257
|
-
if (json)
|
|
257
|
+
if (json)
|
|
258
|
+
return jsonError(
|
|
259
|
+
'Gap reporting is disabled',
|
|
260
|
+
undefined,
|
|
261
|
+
ERROR_CODES.ERR_GAP_REPORT_FAILED,
|
|
262
|
+
);
|
|
258
263
|
humanLog(
|
|
259
264
|
`Gap reporting is disabled (ASTRYX_GAP_REPORT=off or astryx.config.mjs).\n` +
|
|
260
265
|
`Run \`${getRunPrefix()} astryx gap-report setup\` to configure.`,
|
|
@@ -295,7 +300,7 @@ export function registerGapReport(program) {
|
|
|
295
300
|
return;
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
const preview = buildGapReportPreview({
|
|
303
|
+
const preview = await buildGapReportPreview({
|
|
299
304
|
component: options.component,
|
|
300
305
|
category: options.category,
|
|
301
306
|
intention: options.reason,
|
|
@@ -326,7 +331,7 @@ export function registerGapReport(program) {
|
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
try {
|
|
329
|
-
const url = createGapReport({
|
|
334
|
+
const url = await createGapReport({
|
|
330
335
|
component: options.component,
|
|
331
336
|
category: options.category,
|
|
332
337
|
intention: options.reason,
|
|
@@ -343,7 +348,9 @@ export function registerGapReport(program) {
|
|
|
343
348
|
humanLog('\nGap reporting is disabled via configuration.\n');
|
|
344
349
|
}
|
|
345
350
|
} catch (err) {
|
|
346
|
-
cliError(`Filing gap report failed: ${err.message}`, {
|
|
351
|
+
cliError(`Filing gap report failed: ${err.message}`, {
|
|
352
|
+
code: ERROR_CODES.ERR_GAP_REPORT_FAILED,
|
|
353
|
+
});
|
|
347
354
|
return;
|
|
348
355
|
}
|
|
349
356
|
return;
|
|
@@ -386,7 +393,8 @@ export function registerGapReport(program) {
|
|
|
386
393
|
placeholder:
|
|
387
394
|
'e.g. "Need a compact variant for use in dense data tables"',
|
|
388
395
|
validate: val => {
|
|
389
|
-
if (!val.trim())
|
|
396
|
+
if (!val.trim())
|
|
397
|
+
return 'Please describe what you were trying to do';
|
|
390
398
|
},
|
|
391
399
|
}),
|
|
392
400
|
);
|
|
@@ -406,7 +414,7 @@ export function registerGapReport(program) {
|
|
|
406
414
|
source: 'interactive',
|
|
407
415
|
};
|
|
408
416
|
|
|
409
|
-
const preview = buildGapReportPreview(previewArgs);
|
|
417
|
+
const preview = await buildGapReportPreview(previewArgs);
|
|
410
418
|
|
|
411
419
|
// Always show the user exactly what would be filed before sending.
|
|
412
420
|
p.note(
|
|
@@ -440,7 +448,7 @@ export function registerGapReport(program) {
|
|
|
440
448
|
s.start('Filing gap report');
|
|
441
449
|
|
|
442
450
|
try {
|
|
443
|
-
const url = createGapReport(previewArgs);
|
|
451
|
+
const url = await createGapReport(previewArgs);
|
|
444
452
|
s.stop(url ? 'Gap report filed' : 'Gap reporting is disabled');
|
|
445
453
|
if (url) {
|
|
446
454
|
p.log.success(url);
|
|
@@ -40,9 +40,9 @@ describe('shouldActuallyFile', () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it('--dry-run wins over --commit (safety)', () => {
|
|
43
|
-
expect(
|
|
44
|
-
|
|
45
|
-
)
|
|
43
|
+
expect(shouldActuallyFile({isTTY: true, commit: true, dryRun: true})).toBe(
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -54,7 +54,9 @@ describe('formatPreview', () => {
|
|
|
54
54
|
title: '[gap] Button: missing compact variant',
|
|
55
55
|
body: '## User Intention\n\nNeed a compact variant',
|
|
56
56
|
});
|
|
57
|
-
expect(out).toContain(
|
|
57
|
+
expect(out).toContain(
|
|
58
|
+
'Would file GitHub issue on facebookexperimental/xds',
|
|
59
|
+
);
|
|
58
60
|
expect(out).toContain('[gap] Button');
|
|
59
61
|
expect(out).toContain('Need a compact variant');
|
|
60
62
|
expect(out).toContain('Labels: gap-report');
|
|
@@ -91,13 +93,14 @@ describe('ASTRYX_GAP_REPORT=off env var', () => {
|
|
|
91
93
|
it('disables gap reporting when set to "off"', async () => {
|
|
92
94
|
process.env.ASTRYX_GAP_REPORT = 'off';
|
|
93
95
|
vi.resetModules();
|
|
94
|
-
const {loadGapReportConfig, createGapReport} =
|
|
95
|
-
'../utils/github.mjs'
|
|
96
|
-
)
|
|
97
|
-
|
|
96
|
+
const {loadGapReportConfig, createGapReport} =
|
|
97
|
+
await import('../utils/github.mjs');
|
|
98
|
+
await expect(loadGapReportConfig()).resolves.toMatchObject({
|
|
99
|
+
enabled: false,
|
|
100
|
+
});
|
|
98
101
|
|
|
99
102
|
// createGapReport must return null and never invoke gh.
|
|
100
|
-
const result = createGapReport({
|
|
103
|
+
const result = await createGapReport({
|
|
101
104
|
component: 'Button',
|
|
102
105
|
category: 'other',
|
|
103
106
|
intention: 'test',
|
|
@@ -109,12 +112,16 @@ describe('ASTRYX_GAP_REPORT=off env var', () => {
|
|
|
109
112
|
process.env.ASTRYX_GAP_REPORT = 'false';
|
|
110
113
|
vi.resetModules();
|
|
111
114
|
let mod = await import('../utils/github.mjs');
|
|
112
|
-
expect(mod.loadGapReportConfig()
|
|
115
|
+
await expect(mod.loadGapReportConfig()).resolves.toMatchObject({
|
|
116
|
+
enabled: false,
|
|
117
|
+
});
|
|
113
118
|
|
|
114
119
|
process.env.ASTRYX_GAP_REPORT = '0';
|
|
115
120
|
vi.resetModules();
|
|
116
121
|
mod = await import('../utils/github.mjs');
|
|
117
|
-
expect(mod.loadGapReportConfig()
|
|
122
|
+
await expect(mod.loadGapReportConfig()).resolves.toMatchObject({
|
|
123
|
+
enabled: false,
|
|
124
|
+
});
|
|
118
125
|
});
|
|
119
126
|
});
|
|
120
127
|
|
|
@@ -133,16 +140,14 @@ describe('buildGapReportPreview', () => {
|
|
|
133
140
|
it('renders title and body without invoking gh', async () => {
|
|
134
141
|
vi.resetModules();
|
|
135
142
|
const {buildGapReportPreview} = await import('../utils/github.mjs');
|
|
136
|
-
const preview = buildGapReportPreview({
|
|
143
|
+
const preview = await buildGapReportPreview({
|
|
137
144
|
component: 'Button',
|
|
138
145
|
category: 'missing_variant',
|
|
139
146
|
intention: 'Need compact variant for tables',
|
|
140
147
|
});
|
|
141
148
|
expect(preview.mode).toBe('github');
|
|
142
149
|
expect(preview.enabled).toBe(true);
|
|
143
|
-
expect(preview.title).toBe(
|
|
144
|
-
'[gap] Button: Need compact variant for tables',
|
|
145
|
-
);
|
|
150
|
+
expect(preview.title).toBe('[gap] Button: Need compact variant for tables');
|
|
146
151
|
expect(preview.body).toContain('| **Component** | Button |');
|
|
147
152
|
expect(preview.body).toContain('Need compact variant for tables');
|
|
148
153
|
expect(preview.repo).toBe('facebookexperimental/xds');
|
|
@@ -152,7 +157,7 @@ describe('buildGapReportPreview', () => {
|
|
|
152
157
|
process.env.ASTRYX_GAP_REPORT = 'off';
|
|
153
158
|
vi.resetModules();
|
|
154
159
|
const {buildGapReportPreview} = await import('../utils/github.mjs');
|
|
155
|
-
const preview = buildGapReportPreview({
|
|
160
|
+
const preview = await buildGapReportPreview({
|
|
156
161
|
component: 'Button',
|
|
157
162
|
category: 'other',
|
|
158
163
|
intention: 'x',
|
package/src/commands/init.mjs
CHANGED
|
@@ -29,6 +29,37 @@ import {requireInteractive} from '../utils/interactive.mjs';
|
|
|
29
29
|
const VALID_FEATURES = ['agents', 'theme', 'template'];
|
|
30
30
|
const run = getRunPrefix();
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Build the "Next steps" lines printed at the end of `astryx init`.
|
|
34
|
+
*
|
|
35
|
+
* Theme guidance must match the runtime recommendation emitted by core's
|
|
36
|
+
* <Theme> component (packages/core/src/theme/Theme.tsx): the pre-built theme
|
|
37
|
+
* path (`/built` import + `theme.css`) plus the base CSS import, so users
|
|
38
|
+
* don't end up with an unstyled app or the slower runtime style-injection
|
|
39
|
+
* path. See https://github.com/facebook/astryx/issues/3080.
|
|
40
|
+
*
|
|
41
|
+
* Exported for testing.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} runPrefix package-manager run prefix (e.g. `npx`)
|
|
44
|
+
* @returns {string[]} ordered list of human-facing lines
|
|
45
|
+
*/
|
|
46
|
+
export function getNextSteps(runPrefix) {
|
|
47
|
+
return [
|
|
48
|
+
'',
|
|
49
|
+
' Next steps:',
|
|
50
|
+
" 1. Import base styles: import '@astryxdesign/core/reset.css'",
|
|
51
|
+
" and import '@astryxdesign/core/astryx.css'",
|
|
52
|
+
" 2. Import components: import { Button } from '@astryxdesign/core'",
|
|
53
|
+
' 3. Optionally add a theme (use the pre-built path for performance):',
|
|
54
|
+
" import { neutralTheme } from '@astryxdesign/theme-neutral/built'",
|
|
55
|
+
" import '@astryxdesign/theme-neutral/theme.css'",
|
|
56
|
+
' <Theme theme={neutralTheme}>...</Theme>',
|
|
57
|
+
` For custom themes, run \`${runPrefix} astryx theme build <file>\` to generate the built artifacts.`,
|
|
58
|
+
` 4. ${runPrefix} astryx --help for all commands`,
|
|
59
|
+
'',
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
function isCancel(value) {
|
|
33
64
|
if (p.isCancel(value)) {
|
|
34
65
|
p.cancel('Setup cancelled.');
|
|
@@ -100,7 +131,15 @@ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
|
|
|
100
131
|
|
|
101
132
|
if (!interactive) {
|
|
102
133
|
if (!templateName) {
|
|
103
|
-
|
|
134
|
+
// Point agents at the build workflow rather than dumping page-template
|
|
135
|
+
// names — `build` surfaces pages AND blocks AND components for an idea,
|
|
136
|
+
// and `build` with no args is the full how-to-build playbook.
|
|
137
|
+
humanLog('✓ To build UI, use these commands:');
|
|
138
|
+
humanLog('');
|
|
139
|
+
humanLog(` ${run} astryx build "<what you're building>" build a page — kit: closest template + blocks + components`);
|
|
140
|
+
humanLog(` ${run} astryx build the how-to-build workflow (read this first)`);
|
|
141
|
+
humanLog(` ${run} astryx search <query> find anything — components, docs, templates, blocks`);
|
|
142
|
+
humanLog('');
|
|
104
143
|
return;
|
|
105
144
|
}
|
|
106
145
|
|
|
@@ -242,13 +281,8 @@ export function registerInit(program) {
|
|
|
242
281
|
// Outro
|
|
243
282
|
p.outro('Design system initialized!');
|
|
244
283
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
humanLog(' 2. Optionally add a theme:');
|
|
249
|
-
humanLog(" import { neutralTheme } from '@astryxdesign/theme-neutral'");
|
|
250
|
-
humanLog(' <Theme theme={neutralTheme}>...</Theme>');
|
|
251
|
-
humanLog(` 3. ${run} xds --help for all commands`);
|
|
252
|
-
humanLog('');
|
|
284
|
+
for (const line of getNextSteps(run)) {
|
|
285
|
+
humanLog(line);
|
|
286
|
+
}
|
|
253
287
|
});
|
|
254
288
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Regression test for `astryx init` "Next steps" theme guidance.
|
|
5
|
+
*
|
|
6
|
+
* The init command previously steered users toward the slower runtime
|
|
7
|
+
* style-injection path:
|
|
8
|
+
*
|
|
9
|
+
* import { neutralTheme } from '@astryxdesign/theme-neutral'
|
|
10
|
+
* <Theme theme={neutralTheme}>...</Theme>
|
|
11
|
+
*
|
|
12
|
+
* ...which contradicted the runtime console warning emitted by core's
|
|
13
|
+
* <Theme> component (packages/core/src/theme/Theme.tsx) recommending the
|
|
14
|
+
* pre-built path, and left users with an unstyled app because the base CSS
|
|
15
|
+
* imports were never mentioned (facebook/astryx#3080).
|
|
16
|
+
*
|
|
17
|
+
* These assertions lock in the corrected guidance: base CSS imports, the
|
|
18
|
+
* pre-built (`/built` + `theme.css`) theme path, and the custom-theme build
|
|
19
|
+
* command.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {describe, it, expect} from 'vitest';
|
|
23
|
+
import {getNextSteps} from './init.mjs';
|
|
24
|
+
|
|
25
|
+
describe('init Next steps theme guidance', () => {
|
|
26
|
+
const text = getNextSteps('npx').join('\n');
|
|
27
|
+
|
|
28
|
+
it('mentions the base CSS imports so the app is not left unstyled', () => {
|
|
29
|
+
expect(text).toContain("'@astryxdesign/core/reset.css'");
|
|
30
|
+
expect(text).toContain("'@astryxdesign/core/astryx.css'");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses the pre-built theme path matching the runtime recommendation', () => {
|
|
34
|
+
expect(text).toContain("'@astryxdesign/theme-neutral/built'");
|
|
35
|
+
expect(text).toContain("'@astryxdesign/theme-neutral/theme.css'");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('mentions building custom themes via `astryx theme build`', () => {
|
|
39
|
+
expect(text).toContain('astryx theme build <file>');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not steer users to the runtime style-injection import', () => {
|
|
43
|
+
// The bare source import (no `/built`) is the slow runtime-injection path.
|
|
44
|
+
expect(text).not.toContain("from '@astryxdesign/theme-neutral'");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -53,7 +53,7 @@ function parseJson(stdout) {
|
|
|
53
53
|
let tmpDir;
|
|
54
54
|
|
|
55
55
|
beforeEach(() => {
|
|
56
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
56
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-json-contract-'));
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
afterEach(() => {
|
|
@@ -177,9 +177,16 @@ describe('--json contract: supported commands emit valid envelopes', () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
it('astryx upgrade --json (already up to date) emits upgrade.status', () => {
|
|
180
|
-
// Force a no-op range: from >
|
|
180
|
+
// Force a no-op range: from > installed target.
|
|
181
|
+
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
182
|
+
fs.mkdirSync(coreDir, {recursive: true});
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
path.join(coreDir, 'package.json'),
|
|
185
|
+
JSON.stringify({name: '@astryxdesign/core', version: '0.0.1'}, null, 2),
|
|
186
|
+
);
|
|
187
|
+
|
|
181
188
|
const {status, stdout} = runCli(
|
|
182
|
-
['upgrade', '--json', '--from', '99.0.0'
|
|
189
|
+
['upgrade', '--json', '--from', '99.0.0'],
|
|
183
190
|
{cwd: tmpDir},
|
|
184
191
|
);
|
|
185
192
|
expect(status).toBe(0);
|