@astryxdesign/cli 0.1.6 → 0.1.7-canary.04cd8f7
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 +33 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/internationalization.doc.mjs +243 -0
- package/docs/layout.doc.dense.mjs +5 -0
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +5 -5
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +5 -5
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +12 -12
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +10 -10
- package/docs/tokens.doc.dense.mjs +6 -7
- package/docs/tokens.doc.mjs +1 -1
- package/docs/tokens.doc.zh.mjs +6 -7
- package/docs/working-with-ai.doc.mjs +18 -18
- package/package.json +13 -10
- package/src/api/docOverlays.test.mjs +133 -0
- package/src/api/docs.mjs +14 -2
- package/src/api/doctor.mjs +3 -3
- package/src/api/integration-block-exports.test.mjs +240 -0
- package/src/api/template-suffix.test.mjs +246 -0
- package/src/api/template.mjs +104 -28
- package/src/api/validate-integration.mjs +0 -8
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/codemods/transforms/v0.1.7/__tests__/migrate-table-tableprops-to-direct-props.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.7/__tests__/rename-table-renderprops-styles-to-xstyle.test.mjs +112 -0
- package/src/codemods/transforms/v0.1.7/index.mjs +27 -0
- package/src/codemods/transforms/v0.1.7/migrate-table-tableprops-to-direct-props.mjs +188 -0
- package/src/codemods/transforms/v0.1.7/rename-table-renderprops-styles-to-xstyle.mjs +197 -0
- package/src/commands/agent-docs.mjs +21 -8
- package/src/commands/agent-docs.test.mjs +31 -4
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +7 -5
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +48 -152
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +19 -22
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/swizzle.mjs +11 -34
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +9 -6
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/config.mjs +5 -14
- package/src/doc.mjs +27 -0
- package/src/doc.test.mjs +383 -0
- package/src/index.mjs +5 -6
- package/src/integration.mjs +4 -15
- package/src/lib/component-discovery.importpath.test.mjs +59 -0
- package/src/lib/component-discovery.mjs +15 -5
- package/src/lib/component-format.mjs +47 -14
- package/src/lib/component-format.test.mjs +95 -1
- package/src/lib/component-loader.mjs +104 -2
- package/src/lib/componentDocOverlay.test.mjs +111 -0
- package/src/lib/config-schema.mjs +0 -30
- package/src/lib/hook-format.mjs +8 -3
- package/src/lib/term-log.mjs +48 -0
- package/src/lib/xle/registry.mjs +0 -5
- package/src/schemas/doc-schema.mjs +226 -0
- package/src/schemas/template-schema.mjs +47 -0
- package/src/template.mjs +9 -67
- package/src/types/config.d.ts +11 -66
- package/src/types/doc.d.ts +23 -0
- package/src/types/integration.d.ts +7 -18
- package/src/types/template-api.d.ts +14 -50
- package/src/utils/package-manager.mjs +78 -0
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +2 -1
- package/templates/blocks/components/Avatar/AvatarGroup.tsx +5 -7
- package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -6
- package/templates/blocks/components/Avatar/AvatarUserCard.tsx +3 -5
- package/templates/blocks/components/Avatar/AvatarWithImage.tsx +8 -6
- package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -5
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputControlledInput.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputDisabled.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputMentionTrigger.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputMultipleTriggers.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputShowcase.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputSlashCommands.tsx +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +41 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +13 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +78 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +38 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +47 -0
- package/templates/pages/ide/page.tsx +35 -41
- package/templates/pages/theme-showcase/page.tsx +7 -7
- package/templates/themes/neutral/neutralTheme.ts +63 -32
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Migrate Table tableProps to direct root props
|
|
5
|
+
* @see https://github.com/facebook/astryx/issues/3679
|
|
6
|
+
*
|
|
7
|
+
* `tableProps` (typed HTMLAttributes<HTMLTableElement>, nesting HTML
|
|
8
|
+
* attributes one level deep) is deprecated. As of v0.1.7, <Table> honors
|
|
9
|
+
* className/style/xstyle directly and spreads all other BaseProps
|
|
10
|
+
* (id, aria-*, data-*, event handlers, ...) onto the root <table>, with
|
|
11
|
+
* direct props taking precedence over tableProps.
|
|
12
|
+
*
|
|
13
|
+
* This codemod lifts object-literal `tableProps` keys into sibling JSX
|
|
14
|
+
* props:
|
|
15
|
+
*
|
|
16
|
+
* - tableProps={{className: 'x', style: s}} → className="x" style={s}
|
|
17
|
+
* - String-literal keys ('aria-label', 'data-testid') become hyphenated
|
|
18
|
+
* JSX attributes.
|
|
19
|
+
* - Keys that collide with an existing sibling attribute (or fail the
|
|
20
|
+
* attribute-name guard) are kept inside a shrunken tableProps with a
|
|
21
|
+
* trailing TODO comment for manual migration.
|
|
22
|
+
* - Dynamic values (tableProps={props}, tableProps={fn()}, objects with
|
|
23
|
+
* spreads/computed keys/methods) are left untouched with a TODO comment.
|
|
24
|
+
*
|
|
25
|
+
* Only elements whose name resolves to a `Table` import (alias-aware)
|
|
26
|
+
* from an Astryx core source are rewritten. No import changes are needed.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const meta = {
|
|
30
|
+
title: 'Migrate Table tableProps to direct root props',
|
|
31
|
+
description:
|
|
32
|
+
'Lifts object-literal `tableProps` keys on <Table> into sibling JSX props ' +
|
|
33
|
+
'(className, style, id, aria-*, data-*, event handlers). Dynamic or ' +
|
|
34
|
+
'colliding entries are kept and annotated with a TODO comment.',
|
|
35
|
+
pr: '#3679',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Import sources that provide the Astryx Table component. */
|
|
39
|
+
const TABLE_IMPORT_SOURCES = new Set([
|
|
40
|
+
'@astryxdesign/core',
|
|
41
|
+
'@astryxdesign/core/Table',
|
|
42
|
+
'@xds/core',
|
|
43
|
+
'@xds/core/Table',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Keys must be valid JSX attribute names to be lifted. */
|
|
47
|
+
const LIFTABLE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
48
|
+
|
|
49
|
+
const TODO_COMMENT =
|
|
50
|
+
' TODO(astryx): tableProps is deprecated — merge these into direct props manually ';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract the static key name from an object property, or null when the
|
|
54
|
+
* property is not a simple liftable entry (spread, computed key, method,
|
|
55
|
+
* getter/setter, non-string literal key).
|
|
56
|
+
*/
|
|
57
|
+
function getPropertyKeyName(prop) {
|
|
58
|
+
if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') return null;
|
|
59
|
+
// espree-style Property nodes: skip methods and accessors
|
|
60
|
+
if (prop.method || (prop.kind != null && prop.kind !== 'init')) return null;
|
|
61
|
+
if (prop.computed) return null;
|
|
62
|
+
const key = prop.key;
|
|
63
|
+
if (key.type === 'Identifier') return key.name;
|
|
64
|
+
if (
|
|
65
|
+
(key.type === 'StringLiteral' || key.type === 'Literal') &&
|
|
66
|
+
typeof key.value === 'string'
|
|
67
|
+
) {
|
|
68
|
+
return key.value;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function transformer(file, api) {
|
|
74
|
+
const j = api.jscodeshift;
|
|
75
|
+
const root = j(file.source);
|
|
76
|
+
let hasChanges = false;
|
|
77
|
+
|
|
78
|
+
// --- 1. Track local names for the Table import (alias-aware) ---
|
|
79
|
+
const tableLocals = new Set();
|
|
80
|
+
root.find(j.ImportDeclaration).forEach((path) => {
|
|
81
|
+
if (!TABLE_IMPORT_SOURCES.has(path.node.source.value)) return;
|
|
82
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
83
|
+
if (spec.type === 'ImportSpecifier' && spec.imported.name === 'Table') {
|
|
84
|
+
tableLocals.add(spec.local.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (tableLocals.size === 0) return undefined;
|
|
90
|
+
|
|
91
|
+
function attachTodo(attr) {
|
|
92
|
+
if (!attr.comments) attr.comments = [];
|
|
93
|
+
if (attr.comments.some((c) => c.value === TODO_COMMENT)) return;
|
|
94
|
+
attr.comments.push(j.commentBlock(TODO_COMMENT, false, true));
|
|
95
|
+
hasChanges = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildAttributeValue(valueNode) {
|
|
99
|
+
if (
|
|
100
|
+
valueNode.type === 'StringLiteral' ||
|
|
101
|
+
(valueNode.type === 'Literal' && typeof valueNode.value === 'string')
|
|
102
|
+
) {
|
|
103
|
+
return j.stringLiteral(valueNode.value);
|
|
104
|
+
}
|
|
105
|
+
return j.jsxExpressionContainer(valueNode);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- 2. Rewrite tableProps on tracked <Table> elements ---
|
|
109
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
110
|
+
const name = path.node.name;
|
|
111
|
+
const componentName = name.type === 'JSXIdentifier' ? name.name : null;
|
|
112
|
+
if (!componentName || !tableLocals.has(componentName)) return;
|
|
113
|
+
|
|
114
|
+
const attrs = path.node.attributes;
|
|
115
|
+
const tablePropsAttr = attrs.find(
|
|
116
|
+
(a) => a.type === 'JSXAttribute' && a.name?.name === 'tableProps',
|
|
117
|
+
);
|
|
118
|
+
if (!tablePropsAttr) return;
|
|
119
|
+
|
|
120
|
+
const value = tablePropsAttr.value;
|
|
121
|
+
const isObjectLiteral =
|
|
122
|
+
value?.type === 'JSXExpressionContainer' &&
|
|
123
|
+
value.expression.type === 'ObjectExpression';
|
|
124
|
+
|
|
125
|
+
// Dynamic case: tableProps={identifier}, tableProps={fn()}, ... —
|
|
126
|
+
// leave the attribute untouched and warn via a trailing comment
|
|
127
|
+
// (api.report is a stub; comments are the only warning channel).
|
|
128
|
+
if (!isObjectLiteral) {
|
|
129
|
+
attachTodo(tablePropsAttr);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const obj = value.expression;
|
|
134
|
+
|
|
135
|
+
// Objects containing spreads, computed keys, or methods are treated
|
|
136
|
+
// as dynamic: no partial lift, just the TODO comment.
|
|
137
|
+
const allSimple = obj.properties.every(
|
|
138
|
+
(prop) => getPropertyKeyName(prop) !== null,
|
|
139
|
+
);
|
|
140
|
+
if (!allSimple) {
|
|
141
|
+
attachTodo(tablePropsAttr);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const lifted = [];
|
|
146
|
+
const kept = [];
|
|
147
|
+
for (const prop of obj.properties) {
|
|
148
|
+
const keyName = getPropertyKeyName(prop);
|
|
149
|
+
const collidesWithSibling = attrs.some(
|
|
150
|
+
(a) => a.type === 'JSXAttribute' && a.name?.name === keyName,
|
|
151
|
+
);
|
|
152
|
+
const collidesWithLifted = lifted.some(
|
|
153
|
+
(a) => a.name.name === keyName,
|
|
154
|
+
);
|
|
155
|
+
if (
|
|
156
|
+
!LIFTABLE_KEY_RE.test(keyName) ||
|
|
157
|
+
collidesWithSibling ||
|
|
158
|
+
collidesWithLifted
|
|
159
|
+
) {
|
|
160
|
+
kept.push(prop);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
lifted.push(
|
|
164
|
+
j.jsxAttribute(
|
|
165
|
+
j.jsxIdentifier(keyName),
|
|
166
|
+
buildAttributeValue(prop.value),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tablePropsIdx = attrs.indexOf(tablePropsAttr);
|
|
172
|
+
if (kept.length === 0) {
|
|
173
|
+
// All keys lifted — replace tableProps with the sibling attributes.
|
|
174
|
+
attrs.splice(tablePropsIdx, 1, ...lifted);
|
|
175
|
+
hasChanges = true;
|
|
176
|
+
} else {
|
|
177
|
+
// Some keys collide or fail the name guard — keep only those in a
|
|
178
|
+
// shrunken tableProps and flag it for manual migration.
|
|
179
|
+
attrs.splice(tablePropsIdx, 0, ...lifted);
|
|
180
|
+
obj.properties = kept;
|
|
181
|
+
attachTodo(tablePropsAttr);
|
|
182
|
+
if (lifted.length > 0) hasChanges = true;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!hasChanges) return undefined;
|
|
187
|
+
return root.toSource({quote: 'single'});
|
|
188
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Rename Table render-prop `styles` field to `xstyle`
|
|
5
|
+
*
|
|
6
|
+
* As of v0.1.7, the Table plugin render-prop interfaces
|
|
7
|
+
* (`TableRenderProps`, `HeaderRowRenderProps`, `HeaderCellRenderProps`,
|
|
8
|
+
* `BodyRowRenderProps`, `BodyCellRenderProps`, `ScrollWrapperRenderProps`)
|
|
9
|
+
* and the `scrollWrapper` component contract renamed their StyleX array
|
|
10
|
+
* field `styles` -> `xstyle`, matching the prop name sub-components
|
|
11
|
+
* receive it under.
|
|
12
|
+
*
|
|
13
|
+
* Custom plugin authors read `props.styles` and write `styles: [...]`
|
|
14
|
+
* inside their transform functions (`transformTable`, `transformHeaderRow`,
|
|
15
|
+
* `transformHeaderCell`, `transformBodyRow`, `transformBodyCell`,
|
|
16
|
+
* `transformScrollWrapper`). This codemod renames those reads and writes.
|
|
17
|
+
*
|
|
18
|
+
* A blind global `styles` rename is unsafe — `styles` is also the
|
|
19
|
+
* conventional local name for a `stylex.create({...})` bag, which is
|
|
20
|
+
* unrelated. So this transform only rewrites `styles` when it is
|
|
21
|
+
* *scoped to a render-prop object*: a function parameter (or a variable)
|
|
22
|
+
* whose TypeScript type annotation is one of the render-prop interfaces.
|
|
23
|
+
* Within such a scope it rewrites:
|
|
24
|
+
*
|
|
25
|
+
* - `<param>.styles` -> `<param>.xstyle` (member reads/writes)
|
|
26
|
+
* - `{ ..., styles: [...] }` -> `{ ..., xstyle: [...] }`
|
|
27
|
+
* for object literals that carry the render-prop shape (a sibling
|
|
28
|
+
* `htmlProps` key), and object literals assigned/spread from the
|
|
29
|
+
* tracked render-prop binding.
|
|
30
|
+
*
|
|
31
|
+
* Ambiguous cases that can't be resolved from types alone (e.g. a
|
|
32
|
+
* render-prop object passed through an untyped variable, or a `styles`
|
|
33
|
+
* bag from `stylex.create`) are left untouched, so the transform never
|
|
34
|
+
* renames an unrelated `styles`. This mirrors the conservative,
|
|
35
|
+
* scope-limited approach of the sibling
|
|
36
|
+
* `migrate-table-tableprops-to-direct-props` codemod.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export const meta = {
|
|
40
|
+
title: 'Rename Table render-prop `styles` field to `xstyle`',
|
|
41
|
+
description:
|
|
42
|
+
'Renames the `styles` StyleX-array field to `xstyle` on Table plugin ' +
|
|
43
|
+
'render-prop objects (TableRenderProps, HeaderRowRenderProps, ' +
|
|
44
|
+
'HeaderCellRenderProps, BodyRowRenderProps, BodyCellRenderProps, ' +
|
|
45
|
+
'ScrollWrapperRenderProps) inside plugin transform functions. Reads ' +
|
|
46
|
+
'(`props.styles`) and writes (`styles: [...]`) are renamed; unrelated ' +
|
|
47
|
+
'`styles` bindings (e.g. from stylex.create) are left untouched.',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The render-prop interface type names whose `styles` field became `xstyle`. */
|
|
51
|
+
const RENDER_PROP_TYPES = new Set([
|
|
52
|
+
'TableRenderProps',
|
|
53
|
+
'HeaderRowRenderProps',
|
|
54
|
+
'HeaderCellRenderProps',
|
|
55
|
+
'BodyRowRenderProps',
|
|
56
|
+
'BodyCellRenderProps',
|
|
57
|
+
'ScrollWrapperRenderProps',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/** Resolve the base type name from a TS type annotation node. */
|
|
61
|
+
function typeNameOf(typeAnnotation) {
|
|
62
|
+
// `x: TableRenderProps`
|
|
63
|
+
const t = typeAnnotation?.typeAnnotation ?? typeAnnotation;
|
|
64
|
+
if (!t) return null;
|
|
65
|
+
if (t.type === 'TSTypeReference' && t.typeName?.type === 'Identifier') {
|
|
66
|
+
return t.typeName.name;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function transformer(file, api) {
|
|
72
|
+
const j = api.jscodeshift;
|
|
73
|
+
const root = j(file.source);
|
|
74
|
+
let hasChanges = false;
|
|
75
|
+
|
|
76
|
+
// --- 1. Collect binding names typed as a render-prop interface. ---
|
|
77
|
+
// Function/arrow params: (props: TableRenderProps) => ...
|
|
78
|
+
const renderPropBindings = new Set();
|
|
79
|
+
|
|
80
|
+
function collectParam(param) {
|
|
81
|
+
if (
|
|
82
|
+
param.type === 'Identifier' &&
|
|
83
|
+
RENDER_PROP_TYPES.has(typeNameOf(param.typeAnnotation))
|
|
84
|
+
) {
|
|
85
|
+
renderPropBindings.add(param.name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
root
|
|
90
|
+
.find(j.Function)
|
|
91
|
+
.forEach((p) => (p.node.params ?? []).forEach(collectParam));
|
|
92
|
+
root
|
|
93
|
+
.find(j.FunctionDeclaration)
|
|
94
|
+
.forEach((p) => (p.node.params ?? []).forEach(collectParam));
|
|
95
|
+
root
|
|
96
|
+
.find(j.FunctionExpression)
|
|
97
|
+
.forEach((p) => (p.node.params ?? []).forEach(collectParam));
|
|
98
|
+
root
|
|
99
|
+
.find(j.ArrowFunctionExpression)
|
|
100
|
+
.forEach((p) => (p.node.params ?? []).forEach(collectParam));
|
|
101
|
+
|
|
102
|
+
// Variable declarations: `const rp: BodyCellRenderProps = ...`
|
|
103
|
+
root.find(j.VariableDeclarator).forEach((p) => {
|
|
104
|
+
const id = p.node.id;
|
|
105
|
+
if (
|
|
106
|
+
id?.type === 'Identifier' &&
|
|
107
|
+
RENDER_PROP_TYPES.has(typeNameOf(id.typeAnnotation))
|
|
108
|
+
) {
|
|
109
|
+
renderPropBindings.add(id.name);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const hasTypedBinding = renderPropBindings.size > 0;
|
|
114
|
+
|
|
115
|
+
// Also detect object literals that are structurally a render-prop object.
|
|
116
|
+
// Two shapes qualify:
|
|
117
|
+
// 1. An `htmlProps` sibling key alongside a `styles`/`xstyle` key — the
|
|
118
|
+
// literal shape a transform function returns from scratch.
|
|
119
|
+
// 2. A spread of a tracked render-prop binding (`{...props, styles: [...]}`)
|
|
120
|
+
// — the common "carry the render-prop object forward, override styles"
|
|
121
|
+
// shape. (Only counts when `renderPropBindings` is non-empty.)
|
|
122
|
+
function isRenderPropShapedObject(objExpr) {
|
|
123
|
+
if (objExpr.type !== 'ObjectExpression') return false;
|
|
124
|
+
const keyNames = objExpr.properties
|
|
125
|
+
.map((pr) =>
|
|
126
|
+
pr.type === 'ObjectProperty' || pr.type === 'Property'
|
|
127
|
+
? pr.key?.name ?? pr.key?.value
|
|
128
|
+
: null,
|
|
129
|
+
)
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
if (keyNames.includes('htmlProps') && keyNames.includes('styles')) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// Spread of a tracked render-prop binding.
|
|
135
|
+
const spreadsRenderProp = objExpr.properties.some(
|
|
136
|
+
(pr) =>
|
|
137
|
+
(pr.type === 'SpreadElement' || pr.type === 'ExperimentalSpreadProperty') &&
|
|
138
|
+
pr.argument?.type === 'Identifier' &&
|
|
139
|
+
renderPropBindings.has(pr.argument.name),
|
|
140
|
+
);
|
|
141
|
+
return spreadsRenderProp && keyNames.includes('styles');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!hasTypedBinding) {
|
|
145
|
+
// Nothing is typed as a render-prop interface. Only rewrite clearly
|
|
146
|
+
// render-prop-shaped object literals (htmlProps + styles siblings);
|
|
147
|
+
// never touch bare `styles` in this file (too ambiguous).
|
|
148
|
+
let touched = false;
|
|
149
|
+
root.find(j.ObjectExpression).forEach((p) => {
|
|
150
|
+
if (!isRenderPropShapedObject(p.node)) return;
|
|
151
|
+
for (const prop of p.node.properties) {
|
|
152
|
+
if (
|
|
153
|
+
(prop.type === 'ObjectProperty' || prop.type === 'Property') &&
|
|
154
|
+
!prop.computed &&
|
|
155
|
+
prop.key?.type === 'Identifier' &&
|
|
156
|
+
prop.key.name === 'styles'
|
|
157
|
+
) {
|
|
158
|
+
prop.key.name = 'xstyle';
|
|
159
|
+
touched = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (!touched) return undefined;
|
|
164
|
+
return root.toSource({quote: 'single'});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- 2. Rewrite `<binding>.styles` member access -> `.xstyle`. ---
|
|
168
|
+
root.find(j.MemberExpression).forEach((p) => {
|
|
169
|
+
const {object, property, computed} = p.node;
|
|
170
|
+
if (computed) return;
|
|
171
|
+
if (property?.type !== 'Identifier' || property.name !== 'styles') return;
|
|
172
|
+
if (object?.type !== 'Identifier' || !renderPropBindings.has(object.name)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
property.name = 'xstyle';
|
|
176
|
+
hasChanges = true;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- 3. Rewrite `styles:` keys on render-prop-shaped object literals. ---
|
|
180
|
+
root.find(j.ObjectExpression).forEach((p) => {
|
|
181
|
+
if (!isRenderPropShapedObject(p.node)) return;
|
|
182
|
+
for (const prop of p.node.properties) {
|
|
183
|
+
if (
|
|
184
|
+
(prop.type === 'ObjectProperty' || prop.type === 'Property') &&
|
|
185
|
+
!prop.computed &&
|
|
186
|
+
prop.key?.type === 'Identifier' &&
|
|
187
|
+
prop.key.name === 'styles'
|
|
188
|
+
) {
|
|
189
|
+
prop.key.name = 'xstyle';
|
|
190
|
+
hasChanges = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!hasChanges) return undefined;
|
|
196
|
+
return root.toSource({quote: 'single'});
|
|
197
|
+
}
|
|
@@ -22,7 +22,7 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import {findCoreDir, CLI_ROOT} from '../utils/paths.mjs';
|
|
24
24
|
import {assertWithin, PathSafetyError} from '../utils/path-safety.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
26
26
|
import {discoverComponents} from '../lib/component-discovery.mjs';
|
|
27
27
|
import {humanLog} from '../lib/json.mjs';
|
|
28
28
|
import {cliError} from '../lib/cli-error.mjs';
|
|
@@ -144,8 +144,8 @@ export function detectStylingSystem(targetDir) {
|
|
|
144
144
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
145
145
|
* styling path that isn't compiled here.
|
|
146
146
|
*/
|
|
147
|
-
export function generateCompressedIndex(version, {coreDir,
|
|
148
|
-
const run =
|
|
147
|
+
export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css'} = {}) {
|
|
148
|
+
const run = invocation;
|
|
149
149
|
const lines = [MARKER_START];
|
|
150
150
|
|
|
151
151
|
// Component count from live discovery
|
|
@@ -197,6 +197,20 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
197
197
|
lines.push("- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)");
|
|
198
198
|
}
|
|
199
199
|
lines.push('- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.');
|
|
200
|
+
// Self-check — post-generation pass. Validated via vibe tests (internal/vibe-tests/
|
|
201
|
+
// prompt-purity-test): on complex multi-step UIs the rules above alone still leave raw
|
|
202
|
+
// CSS in ~11-13% of runs; a re-read-and-fix pass cuts that ~4x at negligible token cost.
|
|
203
|
+
// The fix names the sanctioned escape hatch for the configured system.
|
|
204
|
+
const selfCheckFix = {
|
|
205
|
+
stylex:
|
|
206
|
+
'replace any className=, style={{…}}, raw <div>/<span> layout, imported .css/@apply, or hardcoded #hex/px with the component or the xstyle prop + a token',
|
|
207
|
+
tailwind:
|
|
208
|
+
'replace any style={{…}}, raw <div>/<span> layout, imported .css/@apply, or hardcoded/arbitrary value (e.g. bg-[#fff], p-[13px]) with the component or a token-backed utility',
|
|
209
|
+
css: 'replace any raw <div>/<span> layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-*|--spacing-*|…))',
|
|
210
|
+
};
|
|
211
|
+
lines.push(
|
|
212
|
+
`- SELF-CHECK before you finish: re-read the file and ${selfCheckFix[stylingSystem] ?? selfCheckFix.css}. If unsure a component/prop exists, run \`astryx component <Name>\` / \`astryx search "<thing>"\`; don't hand-roll CSS.`,
|
|
213
|
+
);
|
|
200
214
|
lines.push('');
|
|
201
215
|
|
|
202
216
|
// Command reference — build/template/component are covered in WORKFLOW above.
|
|
@@ -378,7 +392,7 @@ export function removeAgentDocs(targetDir) {
|
|
|
378
392
|
|
|
379
393
|
/**
|
|
380
394
|
* Programmatic entry point for installing agent docs.
|
|
381
|
-
* Used by the init
|
|
395
|
+
* Used by the init command, upgrade command, and agent-docs command.
|
|
382
396
|
*
|
|
383
397
|
* Strategy (when no agent/paths specified):
|
|
384
398
|
* - Discover all existing agent doc files and update them
|
|
@@ -396,9 +410,9 @@ export function removeAgentDocs(targetDir) {
|
|
|
396
410
|
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
|
|
397
411
|
const coreDir = findCoreDir(targetDir);
|
|
398
412
|
const version = getXdsVersion(coreDir);
|
|
399
|
-
const
|
|
413
|
+
const invocation = getCliInvocation(targetDir);
|
|
400
414
|
const stylingSystem = detectStylingSystem(targetDir);
|
|
401
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang,
|
|
415
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem});
|
|
402
416
|
const written = [];
|
|
403
417
|
|
|
404
418
|
// Explicit paths override everything
|
|
@@ -524,8 +538,7 @@ export function registerAgentDocs(program) {
|
|
|
524
538
|
throw err;
|
|
525
539
|
}
|
|
526
540
|
|
|
527
|
-
const
|
|
528
|
-
const run = `${runPrefix} astryx`;
|
|
541
|
+
const run = getCliInvocation(targetDir);
|
|
529
542
|
|
|
530
543
|
for (const t of targets) {
|
|
531
544
|
humanLog(`✓ ${t}`);
|
|
@@ -43,6 +43,26 @@ describe('generateCompressedIndex', () => {
|
|
|
43
43
|
expect(result).toMatch(/never override --color-/);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it('includes the post-generation self-check rule', () => {
|
|
47
|
+
const result = generateCompressedIndex('1.0.0');
|
|
48
|
+
expect(result).toContain('SELF-CHECK before you finish');
|
|
49
|
+
expect(result).toMatch(/re-read the file/);
|
|
50
|
+
expect(result).toMatch(/don't hand-roll CSS/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('tailors the self-check to the styling system (xstyle for StyleX, not className for Tailwind)', () => {
|
|
54
|
+
// StyleX path: className/inline style are veers; the fix is the xstyle prop + a token
|
|
55
|
+
const stylex = generateCompressedIndex('1.0.0', {stylingSystem: 'stylex'});
|
|
56
|
+
const stylexSelfCheck = stylex.split('\n').find(l => l.includes('SELF-CHECK'));
|
|
57
|
+
expect(stylexSelfCheck).toMatch(/xstyle/);
|
|
58
|
+
expect(stylexSelfCheck).toMatch(/className=/);
|
|
59
|
+
// className IS the system in Tailwind — it must NOT be flagged
|
|
60
|
+
const tailwind = generateCompressedIndex('1.0.0', {stylingSystem: 'tailwind'});
|
|
61
|
+
const tailwindSelfCheck = tailwind.split('\n').find(l => l.includes('SELF-CHECK'));
|
|
62
|
+
expect(tailwindSelfCheck).toBeDefined();
|
|
63
|
+
expect(tailwindSelfCheck).not.toMatch(/className=/);
|
|
64
|
+
});
|
|
65
|
+
|
|
46
66
|
it('defaults to the CSS-variable styling path (no compiler)', () => {
|
|
47
67
|
const result = generateCompressedIndex('1.0.0');
|
|
48
68
|
expect(result).toMatch(/style\/className with tokens/);
|
|
@@ -68,17 +88,24 @@ describe('generateCompressedIndex', () => {
|
|
|
68
88
|
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
69
89
|
});
|
|
70
90
|
|
|
71
|
-
it('states the
|
|
72
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
91
|
+
it('states the invocation once in the CLI header (yarn)', () => {
|
|
92
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'yarn astryx'});
|
|
73
93
|
expect(result).toContain('yarn astryx <cmd>');
|
|
74
94
|
expect(result).not.toContain('npx astryx');
|
|
75
95
|
});
|
|
76
96
|
|
|
77
|
-
it('uses pnpm exec
|
|
78
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
97
|
+
it('uses the pnpm exec invocation', () => {
|
|
98
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'pnpm exec astryx'});
|
|
79
99
|
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
80
100
|
expect(result).not.toContain('npx astryx');
|
|
81
101
|
});
|
|
102
|
+
|
|
103
|
+
it('uses the scoped package for one-off (uninstalled) runs so agents never hit the bare name', () => {
|
|
104
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'npx @astryxdesign/cli'});
|
|
105
|
+
expect(result).toContain('npx @astryxdesign/cli <cmd>');
|
|
106
|
+
// The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
|
|
107
|
+
expect(result).not.toContain('npx astryx <cmd>');
|
|
108
|
+
});
|
|
82
109
|
});
|
|
83
110
|
|
|
84
111
|
describe('detectStylingSystem', () => {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - An updated JS module that references the built className
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* astryx theme build ./src/themes/ocean.ts
|
|
12
|
+
* astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
@@ -17,16 +17,15 @@ import * as path from 'node:path';
|
|
|
17
17
|
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
18
18
|
import {spawn} from 'node:child_process';
|
|
19
19
|
import {createJiti} from 'jiti';
|
|
20
|
-
import {
|
|
20
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
21
21
|
import {
|
|
22
22
|
sanitizeName,
|
|
23
23
|
PathSafetyError,
|
|
24
|
-
isNonInteractive,
|
|
25
24
|
} from '../utils/path-safety.mjs';
|
|
26
25
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
27
26
|
import {cliError} from '../lib/cli-error.mjs';
|
|
28
27
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
29
|
-
import {themeAdd
|
|
28
|
+
import {themeAdd} from '../api/theme-add.mjs';
|
|
30
29
|
|
|
31
30
|
// Import shared theme processing from core. `astryx theme build` MUST produce the
|
|
32
31
|
// exact same CSS as the `<Theme>` runtime, so it has exactly one generation
|
|
@@ -500,7 +499,7 @@ function generateBuiltModule(themeDef, iconInfo) {
|
|
|
500
499
|
.join('\n');
|
|
501
500
|
|
|
502
501
|
return `${iconImport}/**
|
|
503
|
-
* ${themeDef.name} theme — built by \`${
|
|
502
|
+
* ${themeDef.name} theme — built by \`${getCliInvocation()} theme build\`
|
|
504
503
|
* Import the CSS file alongside this module:
|
|
505
504
|
*
|
|
506
505
|
* import { ${toIdentifier(themeDef.name)}Theme } from './${themeDef.name}';
|
|
@@ -1130,7 +1129,7 @@ Or with a <link> tag:
|
|
|
1130
1129
|
if (t.description) humanLog(` ${t.description}`);
|
|
1131
1130
|
}
|
|
1132
1131
|
humanLog('\nUsage:');
|
|
1133
|
-
humanLog(
|
|
1132
|
+
humanLog(` ${getCliInvocation()} theme add <slug> [target-path] Scaffold a theme file you own\n`);
|
|
1134
1133
|
});
|
|
1135
1134
|
|
|
1136
1135
|
theme
|
|
@@ -1141,32 +1140,9 @@ Or with a <link> tag:
|
|
|
1141
1140
|
.action(async (slug, targetPath, options) => {
|
|
1142
1141
|
const json = program.opts().json || false;
|
|
1143
1142
|
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
// ERR_FILE_EXISTS guard.
|
|
1147
|
-
const interactive =
|
|
1148
|
-
!json && !isNonInteractive({json}) && Boolean(process.stdin.isTTY);
|
|
1149
|
-
if (slug && !options.list && !options.overwrite && interactive) {
|
|
1150
|
-
const collision = await detectThemeCollision(slug, targetPath);
|
|
1151
|
-
if (collision) {
|
|
1152
|
-
const rel = path.relative(process.cwd(), collision) || collision;
|
|
1153
|
-
const p = await import('@clack/prompts');
|
|
1154
|
-
const confirmed = await p.confirm({
|
|
1155
|
-
message: `Overwrite existing file ${rel}?`,
|
|
1156
|
-
initialValue: false,
|
|
1157
|
-
});
|
|
1158
|
-
if (p.isCancel(confirmed)) {
|
|
1159
|
-
p.cancel('Cancelled.');
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (!confirmed) {
|
|
1163
|
-
humanLog('Aborted. Re-run with --overwrite to replace the file.');
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
options.overwrite = true;
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1143
|
+
// The CLI is non-interactive: never prompt to confirm an overwrite.
|
|
1144
|
+
// Existing files require an explicit --overwrite; otherwise themeAdd's
|
|
1145
|
+
// ERR_FILE_EXISTS guard rejects the write.
|
|
1170
1146
|
let result;
|
|
1171
1147
|
try {
|
|
1172
1148
|
result = await themeAdd(slug, {
|
|
@@ -1191,7 +1167,7 @@ Or with a <link> tag:
|
|
|
1191
1167
|
if (t.description) humanLog(` ${t.description}`);
|
|
1192
1168
|
}
|
|
1193
1169
|
humanLog('\nUsage:');
|
|
1194
|
-
humanLog(
|
|
1170
|
+
humanLog(` ${getCliInvocation()} theme add <slug> [target-path] Scaffold a theme file you own\n`);
|
|
1195
1171
|
return;
|
|
1196
1172
|
}
|
|
1197
1173
|
|
|
@@ -1219,40 +1195,3 @@ This is your copy of the ${displayName} theme — edit ${entry} to make it your
|
|
|
1219
1195
|
});
|
|
1220
1196
|
}
|
|
1221
1197
|
|
|
1222
|
-
/**
|
|
1223
|
-
* First existing file that scaffolding <slug> into <targetPath> would clobber,
|
|
1224
|
-
* or null. Used to prompt before invoking the API; the API re-validates and
|
|
1225
|
-
* owns any authoritative error.
|
|
1226
|
-
*
|
|
1227
|
-
* @param {string} slug
|
|
1228
|
-
* @param {string} [targetPath]
|
|
1229
|
-
* @returns {Promise<string|null>}
|
|
1230
|
-
*/
|
|
1231
|
-
async function detectThemeCollision(slug, targetPath) {
|
|
1232
|
-
let themes;
|
|
1233
|
-
try {
|
|
1234
|
-
themes = listThemes();
|
|
1235
|
-
} catch {
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
const match = themes.find(t => t.slug.toLowerCase() === slug.toLowerCase());
|
|
1239
|
-
if (!match) return null;
|
|
1240
|
-
|
|
1241
|
-
const rawTarget = targetPath || path.join('src', 'themes', match.slug);
|
|
1242
|
-
let resolvedDir;
|
|
1243
|
-
try {
|
|
1244
|
-
// Fail soft (null) on traversal; the API surfaces the real error.
|
|
1245
|
-
const {assertWithin} = await import('../utils/path-safety.mjs');
|
|
1246
|
-
resolvedDir = assertWithin(rawTarget, process.cwd(), {
|
|
1247
|
-
label: 'theme target path',
|
|
1248
|
-
});
|
|
1249
|
-
} catch {
|
|
1250
|
-
return null;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
for (const name of match.files) {
|
|
1254
|
-
const dest = path.join(resolvedDir, name);
|
|
1255
|
-
if (fs.existsSync(dest)) return dest;
|
|
1256
|
-
}
|
|
1257
|
-
return null;
|
|
1258
|
-
}
|