@astryxdesign/cli 0.1.1 → 0.1.2-canary.2149fa3
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 +19 -8
- package/src/api/discover.mjs +78 -26
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +301 -0
- package/src/api/layout.test.mjs +238 -0
- package/src/api/template.mjs +191 -50
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- 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/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/upgrade.mjs +2 -71
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/index.mjs +6 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +34 -7
- package/src/lib/config.test.mjs +51 -2
- 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 +10 -0
- package/src/lib/package-scanner.mjs +31 -7
- 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/types/config.d.ts +99 -0
- package/src/utils/github.mjs +12 -27
- 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/FileInput/FileInputShowcase.tsx +4 -2
- 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/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/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @xds/cli
|
|
2
2
|
|
|
3
|
+
# 0.1.2
|
|
4
|
+
|
|
5
|
+
#### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- `Text`, `Heading`, `Link`, and `Timestamp` rename the `color="active"` value to `color="accent"`, now mapping to the dedicated `--color-text-accent` token (legible accent text ink) instead of `--color-accent`. Run `astryx upgrade` to migrate call sites automatically. (#2863)
|
|
8
|
+
|
|
9
|
+
#### New Features
|
|
10
|
+
|
|
11
|
+
- Let `astryx.config.mjs` integrations contribute package docs, gap-report hooks, template fetching hooks, upgrade codemods, and post-codemod hooks.
|
|
12
|
+
- Add `astryx theme add <slug> [path]` (and `astryx theme list`) to scaffold a theme's source into your project as editable files you own, with theme sources bundled into the CLI
|
|
13
|
+
|
|
14
|
+
#### Fixes
|
|
15
|
+
|
|
16
|
+
- align `astryx init` theme instructions with the runtime built-theme recommendation (#3080)
|
|
17
|
+
`astryx init` now points users at the pre-built theme path (`@astryxdesign/theme-neutral/built` + `theme.css`) and the base CSS imports, matching the runtime `<Theme>` console guidance, instead of the slower runtime style-injection import that left apps unstyled.
|
|
18
|
+
- `astryx theme build` now derives every output file (.css/.js/.d.ts) from the theme name so they share one naming scheme, shows import paths as bare `./<name>` specifiers (instead of a cwd-rooted `./src/...` path that was wrong when your file already lives under src/), and no longer warns about the `variant` prop on `card`
|
|
19
|
+
|
|
20
|
+
#### Documentation
|
|
21
|
+
|
|
22
|
+
- Rename the ClickableCard and SelectableCard examples to follow the "Component — Variant" title convention (`Clickable Card — Nested Button`, `Selectable Card — Multi-select`), and add playground defaults to both card docs so their docsite previews show realistic card content (#2877)
|
|
23
|
+
- Declare playground scaffolds for the Chat sub-components so they preview at a realistic width (ChatComposer and ChatComposerDrawer wrap in a sized container, and the drawer seeds default content), and drop the redundant visible value label from the ChatComposerDrawer "With Progress" example while keeping the accessible label (#2877)
|
|
24
|
+
- Rename the DateInput "Date Range" example to "Min/Max Constraints" — it demos a single input constrained to a min/max window, not a date-range picker (#2692)
|
|
25
|
+
- Wire local state into more showcase examples that were frozen (static value + no-op onChange): TextInput, TextArea, NumberInput, SegmentedControl, RadioList, Tab, TabList, and TabMenu. Follows the same fix as the Slider/Selector/MultiSelector showcases so the docsite previews are actually interactive
|
|
26
|
+
- Wire local state into the Typeahead, Tokenizer, and FileInput showcase examples (static value + no-op onChange → frozen previews). Completes the interactive-showcase fixes started for Slider/Selector/MultiSelector (#3187-#3189) and the input/tab batch
|
|
27
|
+
- Wire local state into the Slider, Selector, and MultiSelector showcase examples so they are interactive — they were controlled components with a static value and a no-op/missing onChange, so the docsite previews appeared frozen (#3187, #3188, #3189)
|
|
28
|
+
- Add a LinkProvider example block showing how to swap in a framework router link (e.g. Next.js Link) for client-side routing (#2733)
|
|
29
|
+
- Add a showcase block for Outline so its docs page has a hero preview, alongside the existing example blocks (#2871)
|
|
30
|
+
- Remove the "MoreMenu — In Toolbar" example block — it rendered incorrectly and was redundant with the other MoreMenu examples (#2870)
|
|
31
|
+
- Add rendered example blocks for the two column-axis Table plugin hooks,
|
|
32
|
+
shown on their own subcomponent pages:
|
|
33
|
+
- Move the "ToggleButton — Group" example to the ToggleButtonGroup page, where it belongs (it demonstrates grouped toggle behavior) (#2842)
|
|
34
|
+
- Make the Toolbar "Table Filter" example use real Selector controls for its Status and Priority filters instead of buttons styled to look like dropdowns, and add meaningful playground defaults plus richer slot options (buttons, icon buttons, tabs, segmented controls, selectors) to the Toolbar docs (#2877).
|
|
35
|
+
|
|
36
|
+
#### Other Changes
|
|
37
|
+
|
|
38
|
+
- `useTableStickyColumns — Pinned Columns` (on /components/useTableStickyColumns)
|
|
39
|
+
- `useTableColumnResize — Draggable Columns` (on /components/useTableColumnResize)
|
|
40
|
+
|
|
41
|
+
#### Contributors
|
|
42
|
+
|
|
43
|
+
Thanks to everyone who contributed to this release:
|
|
44
|
+
|
|
45
|
+
- @cixzhang
|
|
46
|
+
- @durvesh1992
|
|
47
|
+
- @ejhammond
|
|
48
|
+
- @humbertovirtudes
|
|
49
|
+
- @rubyycheung
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
3
53
|
# 0.1.1
|
|
4
54
|
|
|
5
55
|
#### New Features
|
package/docs/theme.doc.mjs
CHANGED
|
@@ -53,7 +53,7 @@ function App() {
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
type: 'prose',
|
|
56
|
-
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme
|
|
56
|
+
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>`. The same pattern works for every theme; just swap the package and import name.',
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
type: 'prose',
|
|
@@ -261,7 +261,7 @@ const brandTheme = defineTheme({
|
|
|
261
261
|
content: [
|
|
262
262
|
{
|
|
263
263
|
type: 'prose',
|
|
264
|
-
text: 'The `components` field in defineTheme uses semantic component keys and style keys
|
|
264
|
+
text: 'The `components` field in defineTheme uses semantic component keys and style keys, not raw CSS selectors. Use `base` for all instances, `variant:value` or `stateName` for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in `astryx docs styling`.',
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
267
|
type: 'code',
|
|
@@ -34,11 +34,11 @@ export const docs = {
|
|
|
34
34
|
type: 'code',
|
|
35
35
|
lang: 'text',
|
|
36
36
|
label: 'Paste this into your AI',
|
|
37
|
-
code: 'Install @astryxdesign/cli and run `npx astryx
|
|
37
|
+
code: 'Install @astryxdesign/cli and run `npx astryx init --features agents` to set up your Astryx context. Read the generated file.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'prose',
|
|
41
|
-
text: 'That\'s it. The
|
|
41
|
+
text: 'That\'s it. The `init --features agents` command generates everything your AI needs (component index, behavioral rules, CLI reference) pulled from your installed version. After a version bump, run it again to update in place.',
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
type: 'prose',
|
|
@@ -48,9 +48,9 @@ export const docs = {
|
|
|
48
48
|
type: 'code',
|
|
49
49
|
lang: 'bash',
|
|
50
50
|
label: 'Manual options',
|
|
51
|
-
code: `npx astryx
|
|
52
|
-
npx astryx
|
|
53
|
-
npx astryx
|
|
51
|
+
code: `npx astryx init --features agents --agent claude # CLAUDE.md
|
|
52
|
+
npx astryx init --features agents --agent cursor # .cursorrules
|
|
53
|
+
npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
},
|
|
@@ -88,7 +88,7 @@ npx astryx agent-docs --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
|
|
|
88
88
|
lang: 'bash',
|
|
89
89
|
label: 'Install as a Cursor user rule',
|
|
90
90
|
code: `mkdir -p ~/.cursor/rules
|
|
91
|
-
npx astryx
|
|
91
|
+
npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
92
92
|
},
|
|
93
93
|
],
|
|
94
94
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2-canary.2149fa3",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
"./api": {
|
|
39
39
|
"types": "./src/types/api.d.ts",
|
|
40
40
|
"import": "./src/api/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./config": {
|
|
43
|
+
"types": "./src/types/config.d.ts",
|
|
44
|
+
"import": "./src/config.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./xle": {
|
|
47
|
+
"types": "./src/lib/xle/browser.d.ts",
|
|
48
|
+
"import": "./src/lib/xle/browser.mjs"
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
51
|
"files": [
|
|
@@ -51,12 +59,14 @@
|
|
|
51
59
|
"@clack/prompts": "^1.5.1",
|
|
52
60
|
"commander": "^12.1.0",
|
|
53
61
|
"jiti": "^2.7.0",
|
|
54
|
-
"jscodeshift": "^17.3.0"
|
|
62
|
+
"jscodeshift": "^17.3.0",
|
|
63
|
+
"zod": "^4.4.3"
|
|
55
64
|
},
|
|
56
65
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "
|
|
58
|
-
"@astryxdesign/lab": "
|
|
59
|
-
"@astryxdesign/theme-neutral": "
|
|
66
|
+
"@astryxdesign/core": "0.1.2-canary.2149fa3",
|
|
67
|
+
"@astryxdesign/lab": "0.1.2-canary.2149fa3",
|
|
68
|
+
"@astryxdesign/theme-neutral": "0.1.2-canary.2149fa3",
|
|
69
|
+
"gpt-tokenizer": "^2.0.0"
|
|
60
70
|
},
|
|
61
71
|
"peerDependenciesMeta": {
|
|
62
72
|
"@astryxdesign/core": {
|
|
@@ -70,9 +80,10 @@
|
|
|
70
80
|
}
|
|
71
81
|
},
|
|
72
82
|
"devDependencies": {
|
|
73
|
-
"@astryxdesign/core": "
|
|
74
|
-
"@astryxdesign/lab": "
|
|
75
|
-
"@astryxdesign/theme-neutral": "
|
|
83
|
+
"@astryxdesign/core": "0.1.2-canary.2149fa3",
|
|
84
|
+
"@astryxdesign/lab": "0.1.2-canary.2149fa3",
|
|
85
|
+
"@astryxdesign/theme-neutral": "0.1.2-canary.2149fa3",
|
|
86
|
+
"gpt-tokenizer": "^2.0.0"
|
|
76
87
|
},
|
|
77
88
|
"scripts": {
|
|
78
89
|
"astryx": "node bin/astryx.mjs",
|
package/src/api/discover.mjs
CHANGED
|
@@ -5,19 +5,28 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {loadConfig} from '../lib/config.mjs';
|
|
8
|
-
import {
|
|
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
|
|
|
@@ -32,7 +41,7 @@ function validateDocs(docs) {
|
|
|
32
41
|
export async function discover(query, options = {}) {
|
|
33
42
|
const {lang = null, zh = false} = options;
|
|
34
43
|
const config = await loadConfig();
|
|
35
|
-
const toEntry =
|
|
44
|
+
const toEntry = pkg => ({
|
|
36
45
|
name: pkg.name,
|
|
37
46
|
category: pkg.category,
|
|
38
47
|
components: pkg.components,
|
|
@@ -41,11 +50,14 @@ export async function discover(query, options = {}) {
|
|
|
41
50
|
displayName: pkg.displayName,
|
|
42
51
|
});
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
const explicitPackages = (config.loadedIntegrations ?? [])
|
|
54
|
+
.map(integration => integration.package)
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
if (config.packages.length === 0 && explicitPackages.length === 0) {
|
|
45
57
|
return {type: 'discover.list', data: [], meta: {configured: false}};
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
const packages = scanAllPackages(config.packages);
|
|
60
|
+
const packages = scanAllPackages(config.packages, explicitPackages);
|
|
49
61
|
|
|
50
62
|
if (packages.length === 0) {
|
|
51
63
|
return {type: 'discover.list', data: [], meta: {configured: true}};
|
|
@@ -61,7 +73,10 @@ export async function discover(query, options = {}) {
|
|
|
61
73
|
if (slashIdx > 0) {
|
|
62
74
|
const pkgName = query.slice(0, slashIdx);
|
|
63
75
|
const compName = query.slice(slashIdx + 1);
|
|
64
|
-
return await resolveComponentDocs(packages, compName, pkgName, {
|
|
76
|
+
return await resolveComponentDocs(packages, compName, pkgName, {
|
|
77
|
+
lang,
|
|
78
|
+
zh,
|
|
79
|
+
});
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
const pkg = packages.find(p => p.name === query);
|
|
@@ -99,7 +114,16 @@ export async function discover(query, options = {}) {
|
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
if (substringMatches.length > 1) {
|
|
102
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
type: 'discover.search',
|
|
119
|
+
data: {
|
|
120
|
+
query,
|
|
121
|
+
matches: substringMatches.map(m => ({
|
|
122
|
+
package: m.pkg.name,
|
|
123
|
+
component: m.comp,
|
|
124
|
+
})),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// Fuzzy fallback
|
|
@@ -110,7 +134,10 @@ export async function discover(query, options = {}) {
|
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
const fuzzyMatches = allComponents
|
|
113
|
-
.map(item => ({
|
|
137
|
+
.map(item => ({
|
|
138
|
+
...item,
|
|
139
|
+
distance: levenshteinDistance(lower, item.comp.toLowerCase()),
|
|
140
|
+
}))
|
|
114
141
|
.filter(m => m.distance <= 3)
|
|
115
142
|
.sort((a, b) => a.distance - b.distance)
|
|
116
143
|
.slice(0, 5);
|
|
@@ -118,30 +145,46 @@ export async function discover(query, options = {}) {
|
|
|
118
145
|
if (fuzzyMatches.length > 0) {
|
|
119
146
|
throw new AstryxError(
|
|
120
147
|
`"${query}" not found`,
|
|
121
|
-
fuzzyMatches.map(m => ({
|
|
148
|
+
fuzzyMatches.map(m => ({
|
|
149
|
+
name: m.pkg.name + '/' + m.comp,
|
|
150
|
+
reason: 'similar name',
|
|
151
|
+
})),
|
|
122
152
|
ERROR_CODES.ERR_NOT_FOUND,
|
|
123
153
|
);
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
throw new AstryxError(
|
|
156
|
+
throw new AstryxError(
|
|
157
|
+
`"${query}" not found in any package`,
|
|
158
|
+
undefined,
|
|
159
|
+
ERROR_CODES.ERR_NOT_FOUND,
|
|
160
|
+
);
|
|
127
161
|
}
|
|
128
162
|
|
|
129
163
|
async function resolveComponentDocs(packages, compName, pkgName, {lang, zh}) {
|
|
130
164
|
const pkg = packages.find(p => p.name === pkgName);
|
|
131
|
-
if (!pkg)
|
|
165
|
+
if (!pkg)
|
|
166
|
+
throw new AstryxError(
|
|
167
|
+
`Package "${pkgName}" not found`,
|
|
168
|
+
undefined,
|
|
169
|
+
ERROR_CODES.ERR_UNKNOWN_PACKAGE,
|
|
170
|
+
);
|
|
132
171
|
|
|
133
172
|
const result = findComponentInPackages([pkg], compName);
|
|
134
173
|
if (!result) {
|
|
135
174
|
const lower = compName.toLowerCase();
|
|
136
175
|
const hits = pkg.components.filter(c => c.toLowerCase().includes(lower));
|
|
137
|
-
const suggestions =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
const suggestions =
|
|
177
|
+
hits.length > 0
|
|
178
|
+
? hits
|
|
179
|
+
: pkg.components
|
|
180
|
+
.map(c => ({
|
|
181
|
+
name: c,
|
|
182
|
+
distance: levenshteinDistance(lower, c.toLowerCase()),
|
|
183
|
+
}))
|
|
184
|
+
.filter(m => m.distance <= 3)
|
|
185
|
+
.sort((a, b) => a.distance - b.distance)
|
|
186
|
+
.slice(0, 5)
|
|
187
|
+
.map(m => m.name);
|
|
145
188
|
throw new AstryxError(
|
|
146
189
|
`Component "${compName}" not found in ${pkgName}`,
|
|
147
190
|
suggestions.map(s => ({name: s, reason: 'similar name'})),
|
|
@@ -157,9 +200,18 @@ async function loadAndValidate(result, {lang, zh}) {
|
|
|
157
200
|
try {
|
|
158
201
|
docs = await loadDocs(result.docPath, {zh, lang});
|
|
159
202
|
} catch (e) {
|
|
160
|
-
throw new AstryxError(
|
|
203
|
+
throw new AstryxError(
|
|
204
|
+
`Failed to load docs for ${result.componentName}: ${e.message}`,
|
|
205
|
+
undefined,
|
|
206
|
+
ERROR_CODES.ERR_INVALID_DOC,
|
|
207
|
+
);
|
|
161
208
|
}
|
|
162
209
|
const err = validateDocs(docs);
|
|
163
|
-
if (err)
|
|
210
|
+
if (err)
|
|
211
|
+
throw new AstryxError(
|
|
212
|
+
`Invalid docs for ${result.componentName}: ${err}`,
|
|
213
|
+
undefined,
|
|
214
|
+
ERROR_CODES.ERR_INVALID_DOC,
|
|
215
|
+
);
|
|
164
216
|
return {type: 'discover.detail.doc', data: docs};
|
|
165
217
|
}
|
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,301 @@
|
|
|
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 {loadConfig} from '../lib/config.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 `layout.components`
|
|
33
|
+
* (imported by name). App components are how XLE reaches domain pieces — the
|
|
34
|
+
* KpiCard/chart/drawer set that the @astryxdesign/core registry can't see.
|
|
35
|
+
*/
|
|
36
|
+
async function loadBlocks(cwd) {
|
|
37
|
+
const blocks = [];
|
|
38
|
+
try {
|
|
39
|
+
const all = await discoverTemplates(cwd);
|
|
40
|
+
for (const t of all) if (t.type === 'block') blocks.push({...t, kind: 'template'});
|
|
41
|
+
} catch {
|
|
42
|
+
// discovery is best-effort
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const config = await loadConfig(cwd);
|
|
46
|
+
const components = config.layout?.components || {};
|
|
47
|
+
for (const [name, spec] of Object.entries(components)) {
|
|
48
|
+
const importPath = typeof spec === 'string' ? spec : spec.from;
|
|
49
|
+
if (!importPath) continue;
|
|
50
|
+
blocks.push({
|
|
51
|
+
type: 'block',
|
|
52
|
+
kind: 'component',
|
|
53
|
+
dirName: name,
|
|
54
|
+
name,
|
|
55
|
+
description: typeof spec === 'object' ? spec.description || '' : '',
|
|
56
|
+
category: 'app',
|
|
57
|
+
importPath,
|
|
58
|
+
isDefault: typeof spec === 'object' ? Boolean(spec.default) : false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// config is optional
|
|
63
|
+
}
|
|
64
|
+
return blocks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normKey = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
68
|
+
|
|
69
|
+
/** Collect the block names referenced by {hints} anywhere in the doc. */
|
|
70
|
+
function collectHintNames(doc) {
|
|
71
|
+
const names = new Set();
|
|
72
|
+
const visitNode = (node) => {
|
|
73
|
+
if (!node || node.kind === 'group') {
|
|
74
|
+
(node?.children || []).forEach(visit);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (node.hint?.block) names.add(node.hint.block.name);
|
|
78
|
+
for (const slot of node.slots || []) {
|
|
79
|
+
if (slot.value?.hint?.block) names.add(slot.value.hint.block.name);
|
|
80
|
+
(slot.value?.subexpr || []).forEach(visit);
|
|
81
|
+
}
|
|
82
|
+
(node.children || []).forEach(visit);
|
|
83
|
+
};
|
|
84
|
+
const visit = (item) => (item?.kind === 'group' ? item.children.forEach(visit) : visitNode(item));
|
|
85
|
+
doc.roots.forEach(visit);
|
|
86
|
+
doc.overlays.forEach(visit);
|
|
87
|
+
return names;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the blockModules map expand() needs: import-mode for app components,
|
|
92
|
+
* splice-mode (reading + asset-stripping the block source) for template blocks.
|
|
93
|
+
* Only blocks actually referenced are read.
|
|
94
|
+
*/
|
|
95
|
+
function buildBlockModules(doc, blocks) {
|
|
96
|
+
const referenced = collectHintNames(doc);
|
|
97
|
+
if (referenced.size === 0) return new Map();
|
|
98
|
+
const byKey = new Map(blocks.map(b => [normKey(b.dirName), b]));
|
|
99
|
+
const modules = new Map();
|
|
100
|
+
for (const name of referenced) {
|
|
101
|
+
const block = byKey.get(normKey(name));
|
|
102
|
+
if (!block) continue;
|
|
103
|
+
if (block.kind === 'component') {
|
|
104
|
+
modules.set(name, {mode: 'import', componentName: block.name, importPath: block.importPath, isDefault: block.isDefault});
|
|
105
|
+
} else if (block.filePath && fs.existsSync(block.filePath)) {
|
|
106
|
+
modules.set(name, {mode: 'splice', componentName: block.dirName, source: stripTemplateAssetRefs(fs.readFileSync(block.filePath, 'utf-8'))});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return modules;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatIssue(issue) {
|
|
113
|
+
const where = issue.line != null ? `line ${issue.line}: ` : '';
|
|
114
|
+
return `${where}${issue.message}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse + validate, throwing structured XDSErrors on failure.
|
|
119
|
+
* Returns {doc, registry, blocks, warnings}.
|
|
120
|
+
*/
|
|
121
|
+
async function analyze(expression, {form = 'auto', loose = false, cwd = process.cwd()} = {}) {
|
|
122
|
+
const registry = await buildRegistry({cwd});
|
|
123
|
+
const blocks = await loadBlocks(cwd);
|
|
124
|
+
|
|
125
|
+
let doc;
|
|
126
|
+
try {
|
|
127
|
+
doc = parse(expression, {form});
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e instanceof XLEParseError) {
|
|
130
|
+
throw new AstryxError(
|
|
131
|
+
`Layout expression syntax error at line ${e.line}, col ${e.col}: ${e.message}`,
|
|
132
|
+
undefined,
|
|
133
|
+
ERROR_CODES.ERR_LAYOUT_PARSE,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const {errors, warnings} = validate(doc, registry, blocks, {loose});
|
|
140
|
+
return {doc, registry, blocks, errors, warnings};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `astryx layout expand "<expr>" [path]`
|
|
145
|
+
*
|
|
146
|
+
* @param {string} expression
|
|
147
|
+
* @param {object} [options]
|
|
148
|
+
* @param {string} [options.targetPath] - write TSX here (validated against cwd)
|
|
149
|
+
* @param {'compact'|'outline'|'auto'} [options.form]
|
|
150
|
+
* @param {boolean} [options.loose] - downgrade unknown {hints} to TODO warnings
|
|
151
|
+
* @param {string} [options.name] - generated component name
|
|
152
|
+
* @param {string} [options.cwd]
|
|
153
|
+
*/
|
|
154
|
+
export async function layoutExpand(expression, options = {}) {
|
|
155
|
+
const {targetPath, form = 'auto', loose = false, name, cwd = process.cwd()} = options;
|
|
156
|
+
const {doc, registry, blocks, errors, warnings} = await analyze(expression, {form, loose, cwd});
|
|
157
|
+
|
|
158
|
+
if (errors.length > 0) {
|
|
159
|
+
throw new AstryxError(
|
|
160
|
+
`Layout expression is invalid:\n` + errors.map(e => ` - ${formatIssue(e)}`).join('\n'),
|
|
161
|
+
errors.flatMap(e => (e.suggestions || []).map(s => ({name: s, reason: 'did you mean this?'}))),
|
|
162
|
+
ERROR_CODES.ERR_LAYOUT_INVALID,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const componentName = name || 'GeneratedLayout';
|
|
167
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(componentName)) {
|
|
168
|
+
throw new AstryxError(
|
|
169
|
+
`--name must be a PascalCase component name, got '${componentName}'`,
|
|
170
|
+
undefined,
|
|
171
|
+
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const blockModules = buildBlockModules(doc, blocks);
|
|
175
|
+
const result = expand(doc, registry, {componentName, blockModules});
|
|
176
|
+
|
|
177
|
+
let written = null;
|
|
178
|
+
if (targetPath) {
|
|
179
|
+
let resolved;
|
|
180
|
+
try {
|
|
181
|
+
resolved = assertWithin(targetPath, cwd, {label: 'layout target path'});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof PathSafetyError) {
|
|
184
|
+
throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
const filePath = isFilePathArg(targetPath)
|
|
189
|
+
? resolved
|
|
190
|
+
: path.join(resolved, `${componentName}.tsx`);
|
|
191
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
192
|
+
fs.writeFileSync(filePath, result.code);
|
|
193
|
+
written = path.relative(cwd, filePath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
type: 'layout.expand',
|
|
198
|
+
data: {
|
|
199
|
+
form: form === 'auto' ? detectForm(expression) : form,
|
|
200
|
+
code: result.code,
|
|
201
|
+
componentsUsed: result.componentsUsed,
|
|
202
|
+
states: result.states,
|
|
203
|
+
todos: result.todos,
|
|
204
|
+
blocksReferenced: [...blockModules.entries()].map(([name, m]) => ({name, mode: m.mode})),
|
|
205
|
+
warnings: warnings.map(formatIssue),
|
|
206
|
+
written,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `astryx layout check "<expr>" [--form compact|outline]`
|
|
213
|
+
* Validates without expanding; echoes both canonical surfaces.
|
|
214
|
+
*/
|
|
215
|
+
export async function layoutCheck(expression, options = {}) {
|
|
216
|
+
const {form = 'auto', loose = false, cwd = process.cwd()} = options;
|
|
217
|
+
const {doc, errors, warnings} = await analyze(expression, {form, loose, cwd});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
type: 'layout.check',
|
|
221
|
+
data: {
|
|
222
|
+
valid: errors.length === 0,
|
|
223
|
+
form: doc.form,
|
|
224
|
+
errors: errors.map(e => ({...e, formatted: formatIssue(e)})),
|
|
225
|
+
warnings: warnings.map(formatIssue),
|
|
226
|
+
compact: toCompact(doc),
|
|
227
|
+
outline: toOutline(doc),
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `astryx layout grammar` — the agent cheatsheet, with the alias table
|
|
234
|
+
* generated from this branch's registry (never hand-maintained).
|
|
235
|
+
*/
|
|
236
|
+
export async function layoutGrammar(options = {}) {
|
|
237
|
+
const {cwd = process.cwd()} = options;
|
|
238
|
+
const registry = await buildRegistry({cwd});
|
|
239
|
+
|
|
240
|
+
const aliasLines = [];
|
|
241
|
+
const byTarget = new Map();
|
|
242
|
+
for (const [alias, target] of registry.aliases) {
|
|
243
|
+
if (!byTarget.has(target)) byTarget.set(target, []);
|
|
244
|
+
byTarget.get(target).push(alias);
|
|
245
|
+
}
|
|
246
|
+
for (const [target, aliases] of [...byTarget.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
247
|
+
aliasLines.push(`${aliases.join('/')}=${target}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const text = `XLE/XLO — XDS layout expressions (branch-generated; aliases reflect this install)
|
|
251
|
+
|
|
252
|
+
WORKFLOW
|
|
253
|
+
astryx layout check "<expr>" validate; echoes canonical compact + outline forms
|
|
254
|
+
astryx layout expand "<expr>" [path] emit validated TSX (path optional; --name <Pascal>)
|
|
255
|
+
Errors carry line/col + suggestions. Fix and resubmit; nothing is guessed.
|
|
256
|
+
|
|
257
|
+
TWO SURFACES, ONE LANGUAGE (autodetected; --form to force)
|
|
258
|
+
compact: A[cp6 @topNav=TN] > L > LC > S[p6] > (C{card-callout}*4) + T
|
|
259
|
+
outline: indentation = nesting · same-indent = siblings · "repeat N:" block = (...)*N
|
|
260
|
+
slot lines: topNav: TN (or a block: topNav:\\n TN ...)
|
|
261
|
+
|
|
262
|
+
NODE ANATOMY Name#id.enum"payload"[attrs]{hint}*N > children
|
|
263
|
+
.enum unique enum value of any prop: Bd.success Tx.lg B.primary
|
|
264
|
+
"payload" primary text prop (label/title/heading) or text child: TI"Email" B"Save"
|
|
265
|
+
{hint} kebab-case template/component reference (see TEMPLATE REFERENCING) — NEVER text
|
|
266
|
+
*N / xN repeat (use $ for the counter: Tk"item-$"*3)
|
|
267
|
+
trailing ! initial selection for scaffolded state: Tab"Overview"!
|
|
268
|
+
|
|
269
|
+
ATTRS [...] (outline: bare tokens after the name, no brackets)
|
|
270
|
+
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)
|
|
271
|
+
key=value t=email href='/x' c{min:340} dv=[top,bottom] — keys validated per component
|
|
272
|
+
flags req opt dis striped hover divider … (isX/hasX props) · negate: !scroll
|
|
273
|
+
align j= main axis, a= cross axis — expander picks hAlign/vAlign per stack direction
|
|
274
|
+
slots @slotName=Node | @slotName=(sub > expr) | @slotName='text' | @slotName=#id
|
|
275
|
+
trigger opens=#id (a plain attr, no @ — binds an onClick that opens the overlay)
|
|
276
|
+
fill on a stack child → wraps in <StackItem size="fill">
|
|
277
|
+
|
|
278
|
+
TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches past the @astryxdesign/core shell)
|
|
279
|
+
C{card-callout} splice a template block (astryx template --list --type block):
|
|
280
|
+
the block is co-defined once in the file, referenced, imports merged
|
|
281
|
+
{kpi-card} standalone reference (no wrapper element) — place a component directly
|
|
282
|
+
{kpi-card}*4 repeat a reference; the definition/import is emitted once
|
|
283
|
+
app components register local ones in astryx.config.mjs to import them by name:
|
|
284
|
+
export default {layout: {components: {KpiCard: '@/components/KpiCard'}}}
|
|
285
|
+
then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
|
|
286
|
+
|
|
287
|
+
STRUCTURE THE EXPANDER HANDLES
|
|
288
|
+
Layout > LH + LC + LF + LP children auto-route into header/content/footer/start slots
|
|
289
|
+
T > (TR>THC*4) + (TR>TC*4)*6 rows partition into TableHeader/TableBody automatically
|
|
290
|
+
TabList/inputs required value+onChange scaffold typed useState automatically
|
|
291
|
+
overlays compact: tree ;; Dlg#confirm[...] · outline: overlays: section
|
|
292
|
+
trigger: B"Delete"[opens=#confirm]
|
|
293
|
+
|
|
294
|
+
ALIASES (full component names always valid; XDS prefix optional)
|
|
295
|
+
${aliasLines.join(' ')}
|
|
296
|
+
`;
|
|
297
|
+
|
|
298
|
+
return {type: 'layout.grammar', data: {text, aliases: Object.fromEntries(registry.aliases)}};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export {ALIAS_TABLE};
|