@astryxdesign/cli 0.1.6 → 0.1.7-canary.0a6de96

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +97 -7
  3. package/docs/cli-integrations.doc.mjs +150 -0
  4. package/docs/internationalization.doc.mjs +243 -0
  5. package/docs/layout.doc.dense.mjs +5 -0
  6. package/docs/principles.doc.dense.mjs +5 -5
  7. package/docs/principles.doc.zh.mjs +5 -5
  8. package/docs/theme.doc.dense.mjs +12 -12
  9. package/docs/theme.doc.zh.mjs +10 -10
  10. package/docs/tokens.doc.dense.mjs +6 -7
  11. package/docs/tokens.doc.zh.mjs +6 -7
  12. package/package.json +13 -9
  13. package/src/api/docOverlays.test.mjs +133 -0
  14. package/src/api/docs.mjs +14 -2
  15. package/src/api/integration-block-exports.test.mjs +240 -0
  16. package/src/api/template-suffix.test.mjs +246 -0
  17. package/src/api/template.mjs +104 -28
  18. package/src/api/validate-integration.mjs +0 -8
  19. package/src/codemods/__tests__/registry.test.mjs +1 -0
  20. package/src/codemods/registry.mjs +1 -0
  21. package/src/codemods/transforms/v0.1.7/__tests__/migrate-table-tableprops-to-direct-props.test.mjs +120 -0
  22. package/src/codemods/transforms/v0.1.7/__tests__/rename-table-renderprops-styles-to-xstyle.test.mjs +112 -0
  23. package/src/codemods/transforms/v0.1.7/index.mjs +27 -0
  24. package/src/codemods/transforms/v0.1.7/migrate-table-tableprops-to-direct-props.mjs +188 -0
  25. package/src/codemods/transforms/v0.1.7/rename-table-renderprops-styles-to-xstyle.mjs +197 -0
  26. package/src/commands/agent-docs.mjs +14 -0
  27. package/src/commands/agent-docs.test.mjs +20 -0
  28. package/src/config.mjs +5 -14
  29. package/src/doc.mjs +27 -0
  30. package/src/doc.test.mjs +383 -0
  31. package/src/integration.mjs +4 -15
  32. package/src/lib/component-discovery.importpath.test.mjs +59 -0
  33. package/src/lib/component-discovery.mjs +15 -5
  34. package/src/lib/component-format.mjs +45 -13
  35. package/src/lib/component-format.test.mjs +95 -1
  36. package/src/lib/component-loader.mjs +104 -2
  37. package/src/lib/componentDocOverlay.test.mjs +111 -0
  38. package/src/lib/config-schema.mjs +0 -30
  39. package/src/lib/hook-format.mjs +8 -3
  40. package/src/lib/xle/registry.mjs +0 -5
  41. package/src/schemas/doc-schema.mjs +226 -0
  42. package/src/schemas/template-schema.mjs +47 -0
  43. package/src/template.mjs +9 -67
  44. package/src/types/config.d.ts +11 -66
  45. package/src/types/doc.d.ts +23 -0
  46. package/src/types/integration.d.ts +7 -18
  47. package/src/types/template-api.d.ts +14 -50
  48. package/templates/blocks/components/Avatar/AvatarGroup.tsx +5 -7
  49. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -6
  50. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +3 -5
  51. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +8 -6
  52. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -5
  53. package/templates/blocks/components/ChatComposerInput/ChatComposerInputControlledInput.tsx +1 -1
  54. package/templates/blocks/components/ChatComposerInput/ChatComposerInputDisabled.tsx +1 -1
  55. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMentionTrigger.tsx +1 -1
  56. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMultipleTriggers.tsx +1 -1
  57. package/templates/blocks/components/ChatComposerInput/ChatComposerInputShowcase.tsx +1 -1
  58. package/templates/blocks/components/ChatComposerInput/ChatComposerInputSlashCommands.tsx +1 -1
  59. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +14 -0
  60. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +41 -0
  61. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +13 -0
  62. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +78 -0
  63. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +14 -0
  64. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +38 -0
  65. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +14 -0
  66. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +47 -0
  67. package/templates/pages/ide/page.tsx +35 -41
  68. package/templates/pages/theme-showcase/page.tsx +7 -7
  69. package/templates/themes/neutral/neutralTheme.ts +63 -32
@@ -0,0 +1,112 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect} from 'vitest';
4
+
5
+ async function applyTransform(source) {
6
+ const {default: transform} = await import(
7
+ '../rename-table-renderprops-styles-to-xstyle.mjs'
8
+ );
9
+ const jscodeshift = (await import('jscodeshift')).default;
10
+ const j = jscodeshift.withParser('tsx');
11
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
12
+ const file = {source, path: 'test.tsx'};
13
+ const result = transform(file, api);
14
+ return result ?? source;
15
+ }
16
+
17
+ describe('rename-table-renderprops-styles-to-xstyle', () => {
18
+ it('renames `props.styles` reads when the param is typed TableRenderProps', async () => {
19
+ const input = `function transformTable(props: TableRenderProps) {
20
+ return {...props, styles: [...props.styles, tableStyles.base]};
21
+ }`;
22
+ const output = await applyTransform(input);
23
+ expect(output).toContain('...props.xstyle');
24
+ expect(output).toContain('xstyle: [');
25
+ expect(output).not.toMatch(/\bstyles:/);
26
+ expect(output).not.toContain('props.styles');
27
+ });
28
+
29
+ it('renames reads/writes for BodyCellRenderProps', async () => {
30
+ const input = `const transformBodyCell = (props: BodyCellRenderProps) => ({
31
+ ...props,
32
+ styles: [...props.styles, cellStyles.padded],
33
+ });`;
34
+ const output = await applyTransform(input);
35
+ expect(output).toContain('...props.xstyle');
36
+ expect(output).toContain('xstyle: [');
37
+ expect(output).not.toContain('props.styles');
38
+ });
39
+
40
+ it('handles all six render-prop interface names as param types', async () => {
41
+ for (const typeName of [
42
+ 'TableRenderProps',
43
+ 'HeaderRowRenderProps',
44
+ 'HeaderCellRenderProps',
45
+ 'BodyRowRenderProps',
46
+ 'BodyCellRenderProps',
47
+ 'ScrollWrapperRenderProps',
48
+ ]) {
49
+ const input = `function t(p: ${typeName}) { return p.styles; }`;
50
+ const output = await applyTransform(input);
51
+ expect(output).toContain('p.xstyle');
52
+ expect(output).not.toContain('p.styles');
53
+ }
54
+ });
55
+
56
+ it('renames the `styles:` key on a render-prop-shaped object literal (htmlProps + styles siblings)', async () => {
57
+ const input = `const rp = {htmlProps: {}, styles: []};`;
58
+ const output = await applyTransform(input);
59
+ expect(output).toContain('xstyle: []');
60
+ expect(output).not.toMatch(/\bstyles:/);
61
+ });
62
+
63
+ it('does NOT touch an unrelated `styles` from stylex.create', async () => {
64
+ const input = `import * as stylex from '@stylexjs/stylex';
65
+ const styles = stylex.create({base: {color: 'red'}});
66
+ function useStuff() {
67
+ return stylex.props(styles.base);
68
+ }`;
69
+ const output = await applyTransform(input);
70
+ // No render-prop binding, no htmlProps-shaped object -> unchanged
71
+ expect(output).toContain('const styles = stylex.create');
72
+ expect(output).toContain('styles.base');
73
+ expect(output).not.toContain('xstyle');
74
+ });
75
+
76
+ it('does NOT rename `.styles` on an unrelated (untyped) object even when a render-prop binding exists elsewhere', async () => {
77
+ const input = `function transformTable(props: TableRenderProps) {
78
+ const theme = getTheme();
79
+ return {...props, styles: [...props.styles, theme.styles.base]};
80
+ }`;
81
+ const output = await applyTransform(input);
82
+ // props.styles renamed; theme.styles left alone
83
+ expect(output).toContain('...props.xstyle');
84
+ expect(output).toContain('theme.styles.base');
85
+ });
86
+
87
+ it('returns undefined for files with no render-prop usage', async () => {
88
+ const {default: transform} = await import(
89
+ '../rename-table-renderprops-styles-to-xstyle.mjs'
90
+ );
91
+ const jscodeshift = (await import('jscodeshift')).default;
92
+ const j = jscodeshift.withParser('tsx');
93
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
94
+ const source = `const styles = {a: 1};\nconst x = styles.a;`;
95
+ const result = transform({source, path: 'test.tsx'}, api);
96
+ expect(result).toBeUndefined();
97
+ });
98
+
99
+ it('returns undefined for already-migrated code (xstyle only)', async () => {
100
+ const {default: transform} = await import(
101
+ '../rename-table-renderprops-styles-to-xstyle.mjs'
102
+ );
103
+ const jscodeshift = (await import('jscodeshift')).default;
104
+ const j = jscodeshift.withParser('tsx');
105
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
106
+ const source = `function transformTable(props: TableRenderProps) {
107
+ return {...props, xstyle: [...props.xstyle]};
108
+ }`;
109
+ const result = transform({source, path: 'test.tsx'}, api);
110
+ expect(result).toBeUndefined();
111
+ });
112
+ });
@@ -0,0 +1,27 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file v0.1.7 transform manifest
5
+ *
6
+ * Lists all codemods for the v0.1.7 release in the order they should run.
7
+ */
8
+
9
+ import migrateTableTablePropsToDirectProps, {
10
+ meta as migrateTableTablePropsToDirectPropsMeta,
11
+ } from './migrate-table-tableprops-to-direct-props.mjs';
12
+ import renameTableRenderPropsStylesToXstyle, {
13
+ meta as renameTableRenderPropsStylesToXstyleMeta,
14
+ } from './rename-table-renderprops-styles-to-xstyle.mjs';
15
+
16
+ export default [
17
+ {
18
+ name: 'migrate-table-tableprops-to-direct-props',
19
+ transform: migrateTableTablePropsToDirectProps,
20
+ meta: migrateTableTablePropsToDirectPropsMeta,
21
+ },
22
+ {
23
+ name: 'rename-table-renderprops-styles-to-xstyle',
24
+ transform: renameTableRenderPropsStylesToXstyle,
25
+ meta: renameTableRenderPropsStylesToXstyleMeta,
26
+ },
27
+ ];
@@ -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
+ }
@@ -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.
@@ -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/);
package/src/config.mjs CHANGED
@@ -1,18 +1,9 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * Type-preserving helper for the Astryx config file.
5
- *
6
- * This is an intentionally tiny runtime identity function: it returns its
7
- * argument unchanged. Its value is the exported TypeScript surface from
8
- * `@astryxdesign/cli/config`, so config files get editor/type feedback without
9
- * coupling to CLI internals. Validation is NOT performed here — it happens at
10
- * the load boundary (see `loadModuleWithSchema` + `AstryxConfigSchema`).
11
- *
12
- * @template {import('./types/config').AstryxConfig} T
13
- * @param {T} config
14
- * @returns {T}
4
+ * Re-export of the config-authoring helper, which now lives in
5
+ * `@astryxdesign/core/config` so an app's config file gets type feedback
6
+ * without depending on the CLI. Kept here so existing
7
+ * `@astryxdesign/cli/config` imports continue to work unchanged.
15
8
  */
16
- export function createConfig(config) {
17
- return config;
18
- }
9
+ export {createConfig} from '@astryxdesign/core/config';
package/src/doc.mjs ADDED
@@ -0,0 +1,27 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Doc-authoring API (public `@astryxdesign/cli/doc`).
5
+ *
6
+ * The `createComponentDoc`/`createFunctionDoc`/`createDoc` authoring helpers now
7
+ * live in `@astryxdesign/core/authoring` and are re-exported here so existing
8
+ * `@astryxdesign/cli/doc` imports keep working. The Zod load-boundary schemas
9
+ * live in `./schemas/doc-schema.mjs` (core-free) and are re-exported here for
10
+ * back-compat; internal hot-path code imports them from the schema module
11
+ * directly so it never depends on core's built `dist/`.
12
+ */
13
+
14
+ export {
15
+ createComponentDoc,
16
+ createFunctionDoc,
17
+ createDoc,
18
+ } from '@astryxdesign/core/authoring';
19
+
20
+ export {
21
+ ComponentDocKindSchema,
22
+ FunctionDocKindSchema,
23
+ GenericDocKindSchema,
24
+ StampedDocSchema,
25
+ LegacyDocSchema,
26
+ ComponentDocSchema,
27
+ } from './schemas/doc-schema.mjs';