@astryxdesign/cli 0.1.1 → 0.1.2-canary.043f3fd
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 +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +31 -8
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +87 -28
- package/src/api/doctor.mjs +7 -22
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +302 -0
- package/src/api/layout.test.mjs +241 -0
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +421 -122
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- 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 +2 -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 +2 -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__/v0.1.0-ordering.test.mjs +81 -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 +15 -6
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -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/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- 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/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +241 -224
- 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 -239
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +18 -0
- package/src/config.test.mjs +100 -0
- package/src/index.mjs +13 -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 +105 -0
- package/src/lib/error-codes.mjs +18 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +132 -0
- package/src/lib/integrations.test.mjs +135 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +15 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -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/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 +70 -0
- 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 -252
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- 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/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- 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 +242 -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/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -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 -456
- package/src/commands/gap-report.test.mjs +0 -163
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -86
- package/src/lib/config.test.mjs +0 -42
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/src/api/discover.mjs
CHANGED
|
@@ -4,20 +4,29 @@
|
|
|
4
4
|
* @file Programmatic API for the discover command.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import {Project} from '../lib/project.mjs';
|
|
8
|
+
import {
|
|
9
|
+
scanAllPackages,
|
|
10
|
+
findComponentInPackages,
|
|
11
|
+
} from '../lib/package-scanner.mjs';
|
|
9
12
|
import {loadDocs} from '../lib/component-loader.mjs';
|
|
10
13
|
import {levenshteinDistance} from '../lib/string-utils.mjs';
|
|
11
14
|
import {AstryxError} from './error.mjs';
|
|
12
15
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
13
16
|
|
|
14
17
|
function validateDocs(docs) {
|
|
15
|
-
if (!docs || typeof docs !== 'object')
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
if (docs.
|
|
20
|
-
|
|
18
|
+
if (!docs || typeof docs !== 'object')
|
|
19
|
+
return 'docs export is missing or not an object';
|
|
20
|
+
if (typeof docs.name !== 'string' || !docs.name)
|
|
21
|
+
return 'docs.name is missing or not a string';
|
|
22
|
+
if (!docs.usage || typeof docs.usage.description !== 'string')
|
|
23
|
+
return 'docs.usage.description is missing or not a string';
|
|
24
|
+
if (docs.props && !Array.isArray(docs.props))
|
|
25
|
+
return 'docs.props must be an array';
|
|
26
|
+
if (docs.components && !Array.isArray(docs.components))
|
|
27
|
+
return 'docs.components must be an array';
|
|
28
|
+
if (docs.usage?.bestPractices && !Array.isArray(docs.usage.bestPractices))
|
|
29
|
+
return 'docs.usage.bestPractices must be an array';
|
|
21
30
|
return null;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -31,8 +40,8 @@ function validateDocs(docs) {
|
|
|
31
40
|
*/
|
|
32
41
|
export async function discover(query, options = {}) {
|
|
33
42
|
const {lang = null, zh = false} = options;
|
|
34
|
-
const
|
|
35
|
-
const toEntry =
|
|
43
|
+
const project = await Project.load();
|
|
44
|
+
const toEntry = pkg => ({
|
|
36
45
|
name: pkg.name,
|
|
37
46
|
category: pkg.category,
|
|
38
47
|
components: pkg.components,
|
|
@@ -41,11 +50,21 @@ export async function discover(query, options = {}) {
|
|
|
41
50
|
displayName: pkg.displayName,
|
|
42
51
|
});
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
// External packages come from configured integrations that declare a
|
|
54
|
+
// components root. Each becomes a scannable package keyed by its docsDir.
|
|
55
|
+
const explicitPackages = project.loadedIntegrations
|
|
56
|
+
.filter(integration => integration.components)
|
|
57
|
+
.map(integration => ({
|
|
58
|
+
name: integration.name,
|
|
59
|
+
version: integration.version,
|
|
60
|
+
category: integration.name,
|
|
61
|
+
docsDir: integration.components,
|
|
62
|
+
}));
|
|
63
|
+
if (explicitPackages.length === 0) {
|
|
45
64
|
return {type: 'discover.list', data: [], meta: {configured: false}};
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
const packages = scanAllPackages(
|
|
67
|
+
const packages = scanAllPackages([], explicitPackages);
|
|
49
68
|
|
|
50
69
|
if (packages.length === 0) {
|
|
51
70
|
return {type: 'discover.list', data: [], meta: {configured: true}};
|
|
@@ -61,7 +80,10 @@ export async function discover(query, options = {}) {
|
|
|
61
80
|
if (slashIdx > 0) {
|
|
62
81
|
const pkgName = query.slice(0, slashIdx);
|
|
63
82
|
const compName = query.slice(slashIdx + 1);
|
|
64
|
-
return await resolveComponentDocs(packages, compName, pkgName, {
|
|
83
|
+
return await resolveComponentDocs(packages, compName, pkgName, {
|
|
84
|
+
lang,
|
|
85
|
+
zh,
|
|
86
|
+
});
|
|
65
87
|
}
|
|
66
88
|
|
|
67
89
|
const pkg = packages.find(p => p.name === query);
|
|
@@ -99,7 +121,16 @@ export async function discover(query, options = {}) {
|
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
if (substringMatches.length > 1) {
|
|
102
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
type: 'discover.search',
|
|
126
|
+
data: {
|
|
127
|
+
query,
|
|
128
|
+
matches: substringMatches.map(m => ({
|
|
129
|
+
package: m.pkg.name,
|
|
130
|
+
component: m.comp,
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
103
134
|
}
|
|
104
135
|
|
|
105
136
|
// Fuzzy fallback
|
|
@@ -110,7 +141,10 @@ export async function discover(query, options = {}) {
|
|
|
110
141
|
}
|
|
111
142
|
}
|
|
112
143
|
const fuzzyMatches = allComponents
|
|
113
|
-
.map(item => ({
|
|
144
|
+
.map(item => ({
|
|
145
|
+
...item,
|
|
146
|
+
distance: levenshteinDistance(lower, item.comp.toLowerCase()),
|
|
147
|
+
}))
|
|
114
148
|
.filter(m => m.distance <= 3)
|
|
115
149
|
.sort((a, b) => a.distance - b.distance)
|
|
116
150
|
.slice(0, 5);
|
|
@@ -118,30 +152,46 @@ export async function discover(query, options = {}) {
|
|
|
118
152
|
if (fuzzyMatches.length > 0) {
|
|
119
153
|
throw new AstryxError(
|
|
120
154
|
`"${query}" not found`,
|
|
121
|
-
fuzzyMatches.map(m => ({
|
|
155
|
+
fuzzyMatches.map(m => ({
|
|
156
|
+
name: m.pkg.name + '/' + m.comp,
|
|
157
|
+
reason: 'similar name',
|
|
158
|
+
})),
|
|
122
159
|
ERROR_CODES.ERR_NOT_FOUND,
|
|
123
160
|
);
|
|
124
161
|
}
|
|
125
162
|
|
|
126
|
-
throw new AstryxError(
|
|
163
|
+
throw new AstryxError(
|
|
164
|
+
`"${query}" not found in any package`,
|
|
165
|
+
undefined,
|
|
166
|
+
ERROR_CODES.ERR_NOT_FOUND,
|
|
167
|
+
);
|
|
127
168
|
}
|
|
128
169
|
|
|
129
170
|
async function resolveComponentDocs(packages, compName, pkgName, {lang, zh}) {
|
|
130
171
|
const pkg = packages.find(p => p.name === pkgName);
|
|
131
|
-
if (!pkg)
|
|
172
|
+
if (!pkg)
|
|
173
|
+
throw new AstryxError(
|
|
174
|
+
`Package "${pkgName}" not found`,
|
|
175
|
+
undefined,
|
|
176
|
+
ERROR_CODES.ERR_UNKNOWN_PACKAGE,
|
|
177
|
+
);
|
|
132
178
|
|
|
133
179
|
const result = findComponentInPackages([pkg], compName);
|
|
134
180
|
if (!result) {
|
|
135
181
|
const lower = compName.toLowerCase();
|
|
136
182
|
const hits = pkg.components.filter(c => c.toLowerCase().includes(lower));
|
|
137
|
-
const suggestions =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
183
|
+
const suggestions =
|
|
184
|
+
hits.length > 0
|
|
185
|
+
? hits
|
|
186
|
+
: pkg.components
|
|
187
|
+
.map(c => ({
|
|
188
|
+
name: c,
|
|
189
|
+
distance: levenshteinDistance(lower, c.toLowerCase()),
|
|
190
|
+
}))
|
|
191
|
+
.filter(m => m.distance <= 3)
|
|
192
|
+
.sort((a, b) => a.distance - b.distance)
|
|
193
|
+
.slice(0, 5)
|
|
194
|
+
.map(m => m.name);
|
|
145
195
|
throw new AstryxError(
|
|
146
196
|
`Component "${compName}" not found in ${pkgName}`,
|
|
147
197
|
suggestions.map(s => ({name: s, reason: 'similar name'})),
|
|
@@ -157,9 +207,18 @@ async function loadAndValidate(result, {lang, zh}) {
|
|
|
157
207
|
try {
|
|
158
208
|
docs = await loadDocs(result.docPath, {zh, lang});
|
|
159
209
|
} catch (e) {
|
|
160
|
-
throw new AstryxError(
|
|
210
|
+
throw new AstryxError(
|
|
211
|
+
`Failed to load docs for ${result.componentName}: ${e.message}`,
|
|
212
|
+
undefined,
|
|
213
|
+
ERROR_CODES.ERR_INVALID_DOC,
|
|
214
|
+
);
|
|
161
215
|
}
|
|
162
216
|
const err = validateDocs(docs);
|
|
163
|
-
if (err)
|
|
217
|
+
if (err)
|
|
218
|
+
throw new AstryxError(
|
|
219
|
+
`Invalid docs for ${result.componentName}: ${err}`,
|
|
220
|
+
undefined,
|
|
221
|
+
ERROR_CODES.ERR_INVALID_DOC,
|
|
222
|
+
);
|
|
164
223
|
return {type: 'discover.detail.doc', data: docs};
|
|
165
224
|
}
|
package/src/api/doctor.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import {createRequire} from 'node:module';
|
|
|
27
27
|
import {MIN_NODE_VERSION, isNodeVersionSupported} from '../lib/node-version.mjs';
|
|
28
28
|
import {CLI_ROOT, findCoreDir} from '../utils/paths.mjs';
|
|
29
29
|
import {detectPackageManager} from '../utils/package-manager.mjs';
|
|
30
|
-
import {findConfigPath,
|
|
30
|
+
import {findConfigPath, Project} from '../lib/project.mjs';
|
|
31
31
|
import {semverCompare} from '../utils/semver.mjs';
|
|
32
32
|
|
|
33
33
|
const _require = createRequire(import.meta.url);
|
|
@@ -279,8 +279,9 @@ export async function checkConfig(ctx) {
|
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
282
|
+
// Project.load swallows nothing — it surfaces a genuine load failure — but
|
|
283
|
+
// the config check wants to report a bad default export precisely, so we
|
|
284
|
+
// re-import directly to surface a genuine load failure as a FAIL.
|
|
284
285
|
try {
|
|
285
286
|
const {pathToFileURL} = await import('node:url');
|
|
286
287
|
const mod = await import(pathToFileURL(ctx.configPath).href);
|
|
@@ -291,25 +292,9 @@ export async function checkConfig(ctx) {
|
|
|
291
292
|
label: 'astryx.config.mjs',
|
|
292
293
|
status: 'fail',
|
|
293
294
|
message: `astryx.config.mjs default export is not an object (got ${typeof config}).`,
|
|
294
|
-
fix: 'Export a default object from astryx.config.mjs, e.g. `export default {
|
|
295
|
+
fix: 'Export a default object from astryx.config.mjs, e.g. `export default { integrations: [] };`.',
|
|
295
296
|
};
|
|
296
297
|
}
|
|
297
|
-
// Validate that `packages`, if present, is a string or array of strings.
|
|
298
|
-
if (config?.packages !== undefined) {
|
|
299
|
-
const arr = Array.isArray(config.packages)
|
|
300
|
-
? config.packages
|
|
301
|
-
: [config.packages];
|
|
302
|
-
const bad = arr.some(p => typeof p !== 'string');
|
|
303
|
-
if (bad) {
|
|
304
|
-
return {
|
|
305
|
-
id: 'config',
|
|
306
|
-
label: 'astryx.config.mjs',
|
|
307
|
-
status: 'fail',
|
|
308
|
-
message: 'astryx.config.mjs `packages` must be a string or array of strings.',
|
|
309
|
-
fix: 'Set `packages` to a path string or array of path strings.',
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
298
|
return {
|
|
314
299
|
id: 'config',
|
|
315
300
|
label: 'astryx.config.mjs',
|
|
@@ -493,8 +478,8 @@ export async function runChecks(options = {}) {
|
|
|
493
478
|
// Resolve a possible theme key from config (best-effort; never throws).
|
|
494
479
|
let configTheme = null;
|
|
495
480
|
try {
|
|
496
|
-
const
|
|
497
|
-
configTheme =
|
|
481
|
+
const project = await Project.load(cwd);
|
|
482
|
+
configTheme = project.config?.theme ?? null;
|
|
498
483
|
} catch {
|
|
499
484
|
// Best-effort: a missing/invalid config leaves configTheme null.
|
|
500
485
|
}
|
package/src/api/index.mjs
CHANGED
|
@@ -23,6 +23,7 @@ export {component} from './component.mjs';
|
|
|
23
23
|
export {docs} from './docs.mjs';
|
|
24
24
|
export {discover} from './discover.mjs';
|
|
25
25
|
export {template} from './template.mjs';
|
|
26
|
+
export {themeAdd, listThemes} from './theme-add.mjs';
|
|
26
27
|
export {hook} from './hook.mjs';
|
|
27
28
|
export {search} from './search.mjs';
|
|
28
29
|
export {doctor} from './doctor.mjs';
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Programmatic API for the layout command (XLE/XLO).
|
|
5
|
+
*
|
|
6
|
+
* `astryx layout` turns token-compressed layout expressions into validated
|
|
7
|
+
* XDS TSX. Two input surfaces — compact (Emmet-derived XLE) and outline
|
|
8
|
+
* (indentation-based XLO) — share one AST, one validator, and one
|
|
9
|
+
* expander. See the research: pastes P2376666892 (spec) and P2376717669
|
|
10
|
+
* (outline surface).
|
|
11
|
+
*
|
|
12
|
+
* @input expression string (+ options)
|
|
13
|
+
* @output {type, data} envelopes: layout.expand / layout.check / layout.grammar
|
|
14
|
+
* @position api — pure orchestration over lib/xle; command wrapper in commands/layout.mjs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import {AstryxError} from './error.mjs';
|
|
20
|
+
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
21
|
+
import {assertWithin, isFilePathArg, PathSafetyError} from '../utils/path-safety.mjs';
|
|
22
|
+
import {parse, detectForm, XLEParseError} from '../lib/xle/parse.mjs';
|
|
23
|
+
import {validate} from '../lib/xle/validate.mjs';
|
|
24
|
+
import {expand} from '../lib/xle/expand.mjs';
|
|
25
|
+
import {toCompact, toOutline} from '../lib/xle/print.mjs';
|
|
26
|
+
import {buildRegistry, ALIAS_TABLE} from '../lib/xle/registry.mjs';
|
|
27
|
+
import {discoverTemplates, stripTemplateAssetRefs} from './template.mjs';
|
|
28
|
+
import {Project} from '../lib/project.mjs';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The catalog a `{hint}` can resolve to: template blocks (spliced inline) plus
|
|
32
|
+
* any app-registered local components from astryx.config.mjs
|
|
33
|
+
* `experimental.xle.components` (imported by name). App components are how XLE
|
|
34
|
+
* reaches domain pieces — the KpiCard/chart/drawer set that the
|
|
35
|
+
* @astryxdesign/core registry can't see.
|
|
36
|
+
*/
|
|
37
|
+
async function loadBlocks(cwd) {
|
|
38
|
+
const blocks = [];
|
|
39
|
+
try {
|
|
40
|
+
const all = await discoverTemplates(cwd);
|
|
41
|
+
for (const t of all) if (t.type === 'block') blocks.push({...t, kind: 'template'});
|
|
42
|
+
} catch {
|
|
43
|
+
// discovery is best-effort
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const project = await Project.load(cwd);
|
|
47
|
+
const components = project.config.experimental?.xle?.components ?? {};
|
|
48
|
+
for (const [name, spec] of Object.entries(components)) {
|
|
49
|
+
const importPath = spec.from;
|
|
50
|
+
if (!importPath) continue;
|
|
51
|
+
blocks.push({
|
|
52
|
+
type: 'block',
|
|
53
|
+
kind: 'component',
|
|
54
|
+
dirName: name,
|
|
55
|
+
name,
|
|
56
|
+
description: spec.description ?? '',
|
|
57
|
+
category: 'app',
|
|
58
|
+
importPath,
|
|
59
|
+
isDefault: Boolean(spec.default),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// config is optional
|
|
64
|
+
}
|
|
65
|
+
return blocks;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normKey = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
69
|
+
|
|
70
|
+
/** Collect the block names referenced by {hints} anywhere in the doc. */
|
|
71
|
+
function collectHintNames(doc) {
|
|
72
|
+
const names = new Set();
|
|
73
|
+
const visitNode = (node) => {
|
|
74
|
+
if (!node || node.kind === 'group') {
|
|
75
|
+
(node?.children || []).forEach(visit);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (node.hint?.block) names.add(node.hint.block.name);
|
|
79
|
+
for (const slot of node.slots || []) {
|
|
80
|
+
if (slot.value?.hint?.block) names.add(slot.value.hint.block.name);
|
|
81
|
+
(slot.value?.subexpr || []).forEach(visit);
|
|
82
|
+
}
|
|
83
|
+
(node.children || []).forEach(visit);
|
|
84
|
+
};
|
|
85
|
+
const visit = (item) => (item?.kind === 'group' ? item.children.forEach(visit) : visitNode(item));
|
|
86
|
+
doc.roots.forEach(visit);
|
|
87
|
+
doc.overlays.forEach(visit);
|
|
88
|
+
return names;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the blockModules map expand() needs: import-mode for app components,
|
|
93
|
+
* splice-mode (reading + asset-stripping the block source) for template blocks.
|
|
94
|
+
* Only blocks actually referenced are read.
|
|
95
|
+
*/
|
|
96
|
+
function buildBlockModules(doc, blocks) {
|
|
97
|
+
const referenced = collectHintNames(doc);
|
|
98
|
+
if (referenced.size === 0) return new Map();
|
|
99
|
+
const byKey = new Map(blocks.map(b => [normKey(b.dirName), b]));
|
|
100
|
+
const modules = new Map();
|
|
101
|
+
for (const name of referenced) {
|
|
102
|
+
const block = byKey.get(normKey(name));
|
|
103
|
+
if (!block) continue;
|
|
104
|
+
if (block.kind === 'component') {
|
|
105
|
+
modules.set(name, {mode: 'import', componentName: block.name, importPath: block.importPath, isDefault: block.isDefault});
|
|
106
|
+
} else if (block.filePath && fs.existsSync(block.filePath)) {
|
|
107
|
+
modules.set(name, {mode: 'splice', componentName: block.dirName, source: stripTemplateAssetRefs(fs.readFileSync(block.filePath, 'utf-8'))});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return modules;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatIssue(issue) {
|
|
114
|
+
const where = issue.line != null ? `line ${issue.line}: ` : '';
|
|
115
|
+
return `${where}${issue.message}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse + validate, throwing structured XDSErrors on failure.
|
|
120
|
+
* Returns {doc, registry, blocks, warnings}.
|
|
121
|
+
*/
|
|
122
|
+
async function analyze(expression, {form = 'auto', loose = false, cwd = process.cwd()} = {}) {
|
|
123
|
+
const registry = await buildRegistry({cwd});
|
|
124
|
+
const blocks = await loadBlocks(cwd);
|
|
125
|
+
|
|
126
|
+
let doc;
|
|
127
|
+
try {
|
|
128
|
+
doc = parse(expression, {form});
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e instanceof XLEParseError) {
|
|
131
|
+
throw new AstryxError(
|
|
132
|
+
`Layout expression syntax error at line ${e.line}, col ${e.col}: ${e.message}`,
|
|
133
|
+
undefined,
|
|
134
|
+
ERROR_CODES.ERR_LAYOUT_PARSE,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const {errors, warnings} = validate(doc, registry, blocks, {loose});
|
|
141
|
+
return {doc, registry, blocks, errors, warnings};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `astryx layout expand "<expr>" [path]`
|
|
146
|
+
*
|
|
147
|
+
* @param {string} expression
|
|
148
|
+
* @param {object} [options]
|
|
149
|
+
* @param {string} [options.targetPath] - write TSX here (validated against cwd)
|
|
150
|
+
* @param {'compact'|'outline'|'auto'} [options.form]
|
|
151
|
+
* @param {boolean} [options.loose] - downgrade unknown {hints} to TODO warnings
|
|
152
|
+
* @param {string} [options.name] - generated component name
|
|
153
|
+
* @param {string} [options.cwd]
|
|
154
|
+
*/
|
|
155
|
+
export async function layoutExpand(expression, options = {}) {
|
|
156
|
+
const {targetPath, form = 'auto', loose = false, name, cwd = process.cwd()} = options;
|
|
157
|
+
const {doc, registry, blocks, errors, warnings} = await analyze(expression, {form, loose, cwd});
|
|
158
|
+
|
|
159
|
+
if (errors.length > 0) {
|
|
160
|
+
throw new AstryxError(
|
|
161
|
+
`Layout expression is invalid:\n` + errors.map(e => ` - ${formatIssue(e)}`).join('\n'),
|
|
162
|
+
errors.flatMap(e => (e.suggestions || []).map(s => ({name: s, reason: 'did you mean this?'}))),
|
|
163
|
+
ERROR_CODES.ERR_LAYOUT_INVALID,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const componentName = name || 'GeneratedLayout';
|
|
168
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(componentName)) {
|
|
169
|
+
throw new AstryxError(
|
|
170
|
+
`--name must be a PascalCase component name, got '${componentName}'`,
|
|
171
|
+
undefined,
|
|
172
|
+
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const blockModules = buildBlockModules(doc, blocks);
|
|
176
|
+
const result = expand(doc, registry, {componentName, blockModules});
|
|
177
|
+
|
|
178
|
+
let written = null;
|
|
179
|
+
if (targetPath) {
|
|
180
|
+
let resolved;
|
|
181
|
+
try {
|
|
182
|
+
resolved = assertWithin(targetPath, cwd, {label: 'layout target path'});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err instanceof PathSafetyError) {
|
|
185
|
+
throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
const filePath = isFilePathArg(targetPath)
|
|
190
|
+
? resolved
|
|
191
|
+
: path.join(resolved, `${componentName}.tsx`);
|
|
192
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
193
|
+
fs.writeFileSync(filePath, result.code);
|
|
194
|
+
written = path.relative(cwd, filePath);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
type: 'layout.expand',
|
|
199
|
+
data: {
|
|
200
|
+
form: form === 'auto' ? detectForm(expression) : form,
|
|
201
|
+
code: result.code,
|
|
202
|
+
componentsUsed: result.componentsUsed,
|
|
203
|
+
states: result.states,
|
|
204
|
+
todos: result.todos,
|
|
205
|
+
blocksReferenced: [...blockModules.entries()].map(([name, m]) => ({name, mode: m.mode})),
|
|
206
|
+
warnings: warnings.map(formatIssue),
|
|
207
|
+
written,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* `astryx layout check "<expr>" [--form compact|outline]`
|
|
214
|
+
* Validates without expanding; echoes both canonical surfaces.
|
|
215
|
+
*/
|
|
216
|
+
export async function layoutCheck(expression, options = {}) {
|
|
217
|
+
const {form = 'auto', loose = false, cwd = process.cwd()} = options;
|
|
218
|
+
const {doc, errors, warnings} = await analyze(expression, {form, loose, cwd});
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
type: 'layout.check',
|
|
222
|
+
data: {
|
|
223
|
+
valid: errors.length === 0,
|
|
224
|
+
form: doc.form,
|
|
225
|
+
errors: errors.map(e => ({...e, formatted: formatIssue(e)})),
|
|
226
|
+
warnings: warnings.map(formatIssue),
|
|
227
|
+
compact: toCompact(doc),
|
|
228
|
+
outline: toOutline(doc),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* `astryx layout grammar` — the agent cheatsheet, with the alias table
|
|
235
|
+
* generated from this branch's registry (never hand-maintained).
|
|
236
|
+
*/
|
|
237
|
+
export async function layoutGrammar(options = {}) {
|
|
238
|
+
const {cwd = process.cwd()} = options;
|
|
239
|
+
const registry = await buildRegistry({cwd});
|
|
240
|
+
|
|
241
|
+
const aliasLines = [];
|
|
242
|
+
const byTarget = new Map();
|
|
243
|
+
for (const [alias, target] of registry.aliases) {
|
|
244
|
+
if (!byTarget.has(target)) byTarget.set(target, []);
|
|
245
|
+
byTarget.get(target).push(alias);
|
|
246
|
+
}
|
|
247
|
+
for (const [target, aliases] of [...byTarget.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
248
|
+
aliasLines.push(`${aliases.join('/')}=${target}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const text = `XLE/XLO — XDS layout expressions (branch-generated; aliases reflect this install)
|
|
252
|
+
|
|
253
|
+
WORKFLOW
|
|
254
|
+
astryx layout check "<expr>" validate; echoes canonical compact + outline forms
|
|
255
|
+
astryx layout expand "<expr>" [path] emit validated TSX (path optional; --name <Pascal>)
|
|
256
|
+
Errors carry line/col + suggestions. Fix and resubmit; nothing is guessed.
|
|
257
|
+
|
|
258
|
+
TWO SURFACES, ONE LANGUAGE (autodetected; --form to force)
|
|
259
|
+
compact: A[cp6 @topNav=TN] > L > LC > S[p6] > (C{card-callout}*4) + T
|
|
260
|
+
outline: indentation = nesting · same-indent = siblings · "repeat N:" block = (...)*N
|
|
261
|
+
slot lines: topNav: TN (or a block: topNav:\\n TN ...)
|
|
262
|
+
|
|
263
|
+
NODE ANATOMY Name#id.enum"payload"[attrs]{hint}*N > children
|
|
264
|
+
.enum unique enum value of any prop: Bd.success Tx.lg B.primary
|
|
265
|
+
"payload" primary text prop (label/title/heading) or text child: TI"Email" B"Save"
|
|
266
|
+
{hint} kebab-case template/component reference (see TEMPLATE REFERENCING) — NEVER text
|
|
267
|
+
*N / xN repeat (use $ for the counter: Tk"item-$"*3)
|
|
268
|
+
trailing ! initial selection for scaffolded state: Tab"Overview"!
|
|
269
|
+
|
|
270
|
+
ATTRS [...] (outline: bare tokens after the name, no brackets)
|
|
271
|
+
fused p6 g4 c4 w240 h2 cp2 mw960 rg2 cg2 (per-component: padding lives on Card/Section/AppShell.cp — p6 on AppShell/Layout/VStack errors with a correction)
|
|
272
|
+
key=value t=email href='/x' c{min:340} dv=[top,bottom] — keys validated per component
|
|
273
|
+
flags req opt dis striped hover divider … (isX/hasX props) · negate: !scroll
|
|
274
|
+
align j= main axis, a= cross axis — expander picks hAlign/vAlign per stack direction
|
|
275
|
+
slots @slotName=Node | @slotName=(sub > expr) | @slotName='text' | @slotName=#id
|
|
276
|
+
trigger opens=#id (a plain attr, no @ — binds an onClick that opens the overlay)
|
|
277
|
+
fill on a stack child → wraps in <StackItem size="fill">
|
|
278
|
+
|
|
279
|
+
TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches past the @astryxdesign/core shell)
|
|
280
|
+
C{card-callout} splice a template block (astryx template --list --type block):
|
|
281
|
+
the block is co-defined once in the file, referenced, imports merged
|
|
282
|
+
{kpi-card} standalone reference (no wrapper element) — place a component directly
|
|
283
|
+
{kpi-card}*4 repeat a reference; the definition/import is emitted once
|
|
284
|
+
app components register local ones in astryx.config.mjs to import them by name:
|
|
285
|
+
export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}}}}}
|
|
286
|
+
then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
|
|
287
|
+
|
|
288
|
+
STRUCTURE THE EXPANDER HANDLES
|
|
289
|
+
Layout > LH + LC + LF + LP children auto-route into header/content/footer/start slots
|
|
290
|
+
T > (TR>THC*4) + (TR>TC*4)*6 rows partition into TableHeader/TableBody automatically
|
|
291
|
+
TabList/inputs required value+onChange scaffold typed useState automatically
|
|
292
|
+
overlays compact: tree ;; Dlg#confirm[...] · outline: overlays: section
|
|
293
|
+
trigger: B"Delete"[opens=#confirm]
|
|
294
|
+
|
|
295
|
+
ALIASES (full component names always valid; XDS prefix optional)
|
|
296
|
+
${aliasLines.join(' ')}
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
return {type: 'layout.grammar', data: {text, aliases: Object.fromEntries(registry.aliases)}};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export {ALIAS_TABLE};
|