@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.dea6813

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 (31) hide show
  1. package/README.md +57 -57
  2. package/api/component/component.doc.mjs +1 -1
  3. package/api/component/component.mjs +3 -3
  4. package/api/component/component.type.d.mts +4 -5
  5. package/api/component/component.type.mjs +4 -5
  6. package/api/component/detail/blocks/blocks.d.mts +1 -2
  7. package/api/component/detail/blocks/blocks.mjs +3 -4
  8. package/api/component/list/list.d.mts +5 -0
  9. package/api/component/list/list.mjs +9 -37
  10. package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +165 -86
  11. package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +210 -72
  12. package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +115 -83
  13. package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +186 -57
  14. package/clients/cli/commands/component-package.test.mjs +0 -28
  15. package/foundation/response/response-types.doc.mjs +7 -7
  16. package/package.json +9 -9
  17. package/assets/codemods/transform-prop.mjs +0 -109
  18. package/assets/codemods/transform-prop.test.mjs +0 -95
  19. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +0 -14
  20. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +0 -61
  21. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +0 -14
  22. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +0 -126
  23. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +0 -15
  24. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +0 -86
  25. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +0 -14
  26. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +0 -99
  27. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +0 -14
  28. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +0 -122
  29. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +0 -14
  30. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +0 -95
  31. package/foundation/response/response-types.doc.test.mjs +0 -158
@@ -1,129 +1,161 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- import {describe, expect, it} from 'vitest';
4
- import transform from '../rename-avatar-size-scale.mjs';
3
+ import {describe, it, expect} from 'vitest';
5
4
 
6
5
  async function applyTransform(source) {
6
+ const {default: transform} = await import('../rename-avatar-size-scale.mjs');
7
7
  const jscodeshift = (await import('jscodeshift')).default;
8
8
  const j = jscodeshift.withParser('tsx');
9
9
  const api = {jscodeshift: j, stats: () => {}, report: () => {}};
10
- return transform({source, path: 'test.tsx'}, api) ?? source;
10
+ const file = {source, path: 'test.tsx'};
11
+ const result = transform(file, api);
12
+ return result ?? source;
11
13
  }
12
14
 
13
15
  describe('rename-avatar-size-scale', () => {
14
- it('renames every direct Avatar size literal', async () => {
15
- const output =
16
- await applyTransform(`import {Avatar} from '@astryxdesign/core';
16
+ it('renames every named size on an Avatar JSX size attribute', async () => {
17
+ const input = `import {Avatar} from '@astryxdesign/core';
17
18
  const a = <Avatar name="A" size="tiny" />;
18
19
  const b = <Avatar name="B" size="xsmall" />;
19
20
  const c = <Avatar name="C" size="small" />;
20
21
  const d = <Avatar name="D" size="medium" />;
21
- const e = <Avatar name="E" size="large" />;`);
22
-
23
- for (const size of ['xsm', 'sm', 'md', 'lg', 'xl']) {
24
- expect(output).toContain(`size='${size}'`);
22
+ const e = <Avatar name="E" size="large" />;`;
23
+ const output = await applyTransform(input);
24
+ expect(output).toContain(`size='xsm'`);
25
+ expect(output).toContain(`size='sm'`);
26
+ expect(output).toContain(`size='md'`);
27
+ expect(output).toContain(`size='lg'`);
28
+ expect(output).toContain(`size='xl'`);
29
+ for (const old of ['tiny', 'xsmall', 'small', 'medium', 'large']) {
30
+ expect(output).not.toContain(`size="${old}"`);
25
31
  }
26
32
  });
27
33
 
28
- it('renames literals inside satisfies expressions while preserving the type', async () => {
29
- const output =
30
- await applyTransform(`import {Avatar} from '@astryxdesign/core';
31
- const view = <Avatar size={'small' satisfies AvatarSize} />;`);
32
-
33
- expect(output).toContain("{'md' satisfies AvatarSize}");
34
+ it('renames size on AvatarGroup', async () => {
35
+ const input = `import {AvatarGroup} from '@astryxdesign/core';
36
+ const x = <AvatarGroup size="medium">{kids}</AvatarGroup>;`;
37
+ const output = await applyTransform(input);
38
+ expect(output).toContain(`size='lg'`);
34
39
  });
35
40
 
36
- it('renames AvatarGroup and conditional literals', async () => {
37
- const output =
38
- await applyTransform(`import {AvatarGroup} from '@astryxdesign/core';
39
- const view = <AvatarGroup size={large ? 'large' : 'small'}>{kids}</AvatarGroup>;`);
40
-
41
- expect(output).toContain("large ? 'xl' : 'md'");
41
+ it('leaves numeric sizes untouched', async () => {
42
+ const input = `import {Avatar} from '@astryxdesign/core';
43
+ const x = <Avatar name="A" size={48} />;`;
44
+ const output = await applyTransform(input);
45
+ expect(output).toContain('size={48}');
42
46
  });
43
47
 
44
- it('maps import aliases and default subpath imports', async () => {
45
- const output =
46
- await applyTransform(`import {Avatar as Face} from '@xds/core';
47
- import Faces from '@astryxdesign/core/AvatarGroup';
48
- const view = <><Face size="medium" /><Faces size="large" /></>;`);
49
-
50
- expect(output).toContain("<Face size='lg'");
51
- expect(output).toContain("<Faces size='xl'");
48
+ it('is alias-aware', async () => {
49
+ const input = `import {Avatar as Face} from '@astryxdesign/core';
50
+ const x = <Face name="A" size="small" />;`;
51
+ const output = await applyTransform(input);
52
+ expect(output).toContain(`size='md'`);
52
53
  });
53
54
 
54
- it('ignores declaration-level type imports paired with a local Avatar', async () => {
55
- const input = `import type {Avatar} from '@astryxdesign/core';
56
- const Avatar = props => <div {...props} />;
57
- const view = <Avatar size="small" />;`;
58
-
59
- expect(await applyTransform(input)).toBe(input);
55
+ it('renames the subpath and legacy import sources', async () => {
56
+ const sub = `import {Avatar} from '@astryxdesign/core/Avatar';
57
+ const x = <Avatar name="A" size="large" />;`;
58
+ expect(await applyTransform(sub)).toContain(`size='xl'`);
59
+ const legacy = `import {Avatar} from '@xds/core';
60
+ const x = <Avatar name="A" size="tiny" />;`;
61
+ expect(await applyTransform(legacy)).toContain(`size='xsm'`);
60
62
  });
61
63
 
62
- it('ignores mixed type specifiers while migrating runtime imports', async () => {
63
- const output =
64
- await applyTransform(`import {type Avatar, AvatarGroup} from '@astryxdesign/core';
65
- const Avatar = props => <div {...props} />;
66
- const view = <><Avatar size="small" /><AvatarGroup size="large" /></>;`);
64
+ it('renames a size inside a ternary on an Avatar element', async () => {
65
+ const input = `import {Avatar} from '@astryxdesign/core';
66
+ const x = <Avatar name="A" size={big ? 'large' : 'small'} />;`;
67
+ const output = await applyTransform(input);
68
+ expect(output).toContain(`big ? 'xl' : 'md'`);
69
+ });
67
70
 
68
- expect(output).toContain('<Avatar size="small"');
69
- expect(output).toContain("<AvatarGroup size='xl'");
71
+ it('renames the UNIQUE names tiny/xsmall in Storybook options and size args', async () => {
72
+ const input = `import {Avatar} from '@astryxdesign/core';
73
+ const meta = {
74
+ argTypes: {size: {control: 'select', options: ['tiny', 'xsmall']}},
75
+ args: {size: 'tiny'},
76
+ };`;
77
+ const output = await applyTransform(input);
78
+ expect(output).toContain(`['xsm', 'sm']`);
79
+ expect(output).toContain(`size: 'xsm'`);
70
80
  });
71
81
 
72
- it('maps namespace imports', async () => {
73
- const output =
74
- await applyTransform(`import * as Core from '@astryxdesign/core';
75
- const view = <Core.Avatar size="small" />;`);
82
+ it('renames a FULL Storybook options array (unique name unlocks ambiguous members)', async () => {
83
+ // The presence of a unique name (tiny/xsmall) proves the whole array is the
84
+ // Avatar size enum, so small/medium/large in it are safe to rename too.
85
+ const input = `import {Avatar} from '@astryxdesign/core';
86
+ const meta = {
87
+ argTypes: {size: {control: 'select', options: ['tiny', 'xsmall', 'small', 'medium', 'large']}},
88
+ };`;
89
+ const output = await applyTransform(input);
90
+ expect(output).toContain(`['xsm', 'sm', 'md', 'lg', 'xl']`);
91
+ });
76
92
 
77
- expect(output).toContain("size='md'");
93
+ it('renames a standalone Avatar-size array literal used with .map()', async () => {
94
+ const input = `import {AvatarGroup} from '@astryxdesign/core';
95
+ const sizes = (['tiny', 'xsmall', 'small', 'medium', 'large'] as const).map(s => s);`;
96
+ const output = await applyTransform(input);
97
+ expect(output).toContain(`['xsm', 'sm', 'md', 'lg', 'xl']`);
78
98
  });
79
99
 
80
- it('leaves numeric and dynamic sizes untouched', async () => {
100
+ it('does NOT rename an array of ambiguous words with no unique Avatar name', async () => {
101
+ // Without tiny/xsmall, the array could be anything (priorities, densities),
102
+ // so ambiguous members are left untouched.
81
103
  const input = `import {Avatar} from '@astryxdesign/core';
82
- const named = 'small';
83
- const a = <Avatar size={48} />;
84
- const b = <Avatar size={named} />;`;
85
- expect(await applyTransform(input)).toBe(input);
104
+ const densities = ['small', 'medium', 'large'] as const;`;
105
+ const output = await applyTransform(input);
106
+ expect(output).toContain(`['small', 'medium', 'large']`);
86
107
  });
87
108
 
88
- it('leaves objects, types, arrays, and Storybook metadata untouched', async () => {
109
+ it('renames a UNIQUE name in a size-typed union literal', async () => {
89
110
  const input = `import {Avatar} from '@astryxdesign/core';
90
- type Density = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large';
91
- const card = {size: 'small'};
92
- const sizes = ['tiny', 'xsmall', 'small', 'medium', 'large'];
93
- const meta = {argTypes: {size: {options: sizes}}};
94
- const view = <Avatar size={card.size} />;`;
95
-
96
- expect(await applyTransform(input)).toBe(input);
111
+ type Props = {size: 'tiny' | 'xsmall'};`;
112
+ const output = await applyTransform(input);
113
+ expect(output).toContain(`'xsm' | 'sm'`);
97
114
  });
98
115
 
99
- it('does not touch files without an Avatar import', async () => {
100
- const input = `import {Badge} from '@astryxdesign/core';
101
- const view = <Badge size="small" />;`;
102
- expect(await applyTransform(input)).toBe(input);
103
- });
116
+ // --- Precision guards: ambiguous common words must NOT be corrupted in
117
+ // --- context-blind positions, even in files that import Avatar. ---
104
118
 
105
- it('ignores same-named components imported from other packages', async () => {
106
- const input = `import {Avatar} from '@other/core';
107
- const view = <Avatar size="small" />;`;
108
- expect(await applyTransform(input)).toBe(input);
119
+ it('does NOT rename ambiguous names in an unrelated union type', async () => {
120
+ const input = `import {Avatar} from '@astryxdesign/core';
121
+ type TaskPriority = 'urgent' | 'high' | 'medium' | 'low' | 'none';`;
122
+ const output = await applyTransform(input);
123
+ expect(output).toContain(`'medium'`);
124
+ expect(output).not.toContain(`'lg'`);
109
125
  });
110
126
 
111
- it('ignores a local component that shadows an imported alias', async () => {
112
- const output =
113
- await applyTransform(`import {Avatar as Face} from '@xds/core';
114
- const migrated = <Face size="medium" />;
115
- function Local(Face) { return <Face size="large" />; }`);
127
+ it('does NOT rename an ambiguous name in a non-size object property', async () => {
128
+ const input = `import {Avatar} from '@astryxdesign/core';
129
+ const cfg = {priority: 'medium', density: 'large'};`;
130
+ const output = await applyTransform(input);
131
+ expect(output).toContain(`priority: 'medium'`);
132
+ expect(output).toContain(`density: 'large'`);
133
+ });
116
134
 
117
- expect(output).toContain("size='lg'");
118
- expect(output).toContain('size="large"');
135
+ it('does NOT rename an ambiguous name in a size-keyed object property (component unknown)', async () => {
136
+ // A `size: 'small'` object entry could belong to any component; renaming it
137
+ // blind would be unsafe, so it is left for manual migration.
138
+ const input = `import {Avatar} from '@astryxdesign/core';
139
+ const args = {size: 'small'};`;
140
+ const output = await applyTransform(input);
141
+ expect(output).toContain(`size: 'small'`);
119
142
  });
120
143
 
121
- it('leaves unrelated props on Avatar untouched', async () => {
122
- const output =
123
- await applyTransform(`import {Avatar} from '@astryxdesign/core';
124
- const view = <Avatar size="small" label="small" />;`);
144
+ it('does not touch files that never import Avatar/AvatarGroup', async () => {
145
+ const input = `import {Badge} from '@astryxdesign/core';
146
+ const x = <Badge size="small" />;
147
+ type P = 'tiny' | 'xsmall';`;
148
+ const output = await applyTransform(input);
149
+ expect(output).toContain(`size="small"`);
150
+ expect(output).toContain(`'tiny' | 'xsmall'`);
151
+ });
125
152
 
126
- expect(output).toContain("size='md'");
127
- expect(output).toContain('label="small"');
153
+ it('does not rename an unrelated non-size string in an Avatar file', async () => {
154
+ const input = `import {Avatar} from '@astryxdesign/core';
155
+ const label = 'small';
156
+ const x = <Avatar name="A" size="small" />;`;
157
+ const output = await applyTransform(input);
158
+ expect(output).toContain(`const label = 'small'`);
159
+ expect(output).toContain(`size='md'`);
128
160
  });
129
161
  });
@@ -4,23 +4,46 @@
4
4
  * @file Codemod: Rename Avatar named sizes to Icon's abbreviated scale
5
5
  * @see https://github.com/facebook/astryx/issues/2672
6
6
  *
7
- * This transform is deliberately limited to static literals written directly
8
- * in the `size` prop of an imported Avatar or AvatarGroup JSX element. It does
9
- * not infer through variables, helpers, objects, types, Storybook metadata, or
10
- * wrappers: missing an indirect migration is safer than rewriting another
11
- * component's size vocabulary.
7
+ * As of v0.1.8, Avatar and AvatarGroup use the same abbreviated size scale as
8
+ * Icon (`xsm`/`sm`/`md`/`lg`/`xl`) instead of full words. Pixel values are
9
+ * unchanged — only the names move:
10
+ *
11
+ * tiny (20px) → xsm
12
+ * xsmall (24px) → sm
13
+ * small (36px) → md ← also the new default (was `small`)
14
+ * medium (48px) → lg
15
+ * large (128px) → xl
16
+ *
17
+ * The default size shifts from `small` to `md`, but that is the SAME 36px, so
18
+ * call sites that relied on the default need no change.
19
+ *
20
+ * ## Precision
21
+ *
22
+ * `small`, `medium`, and `large` are common English words that appear as
23
+ * unrelated string/type literals (priorities, statuses, breakpoints, …). To
24
+ * avoid corrupting those, the three AMBIGUOUS names are renamed ONLY in a
25
+ * precise context: a `size` JSX attribute on a component that resolves to an
26
+ * Avatar/AvatarGroup import (alias-aware).
27
+ *
28
+ * `tiny` and `xsmall` are unique to Avatar's scale, so they are additionally
29
+ * renamed in context-blind positions (object properties keyed `size`/`*Size`,
30
+ * Storybook `options` arrays, and size-typed union literals) within files that
31
+ * import Avatar/AvatarGroup.
12
32
  */
13
33
 
14
- import {transformProp} from '../../transform-prop.mjs';
15
-
16
34
  export const meta = {
17
35
  title: 'Rename Avatar named sizes tiny/xsmall/small/medium/large → xsm/sm/md/lg/xl',
18
36
  description:
19
- 'Renames static sizes written directly on imported Avatar and AvatarGroup ' +
20
- 'JSX elements. Indirect values are left unchanged for manual migration.',
37
+ 'Avatar and AvatarGroup adopt Icon\'s abbreviated size scale ' +
38
+ '(xsm/sm/md/lg/xl). Pixel values are unchanged; the default moves from ' +
39
+ '`small` to `md` (both 36px). The ambiguous names small/medium/large are ' +
40
+ 'only renamed on Avatar/AvatarGroup `size` JSX attributes; the unique ' +
41
+ 'names tiny/xsmall are also renamed in size-keyed props, Storybook ' +
42
+ 'options, and union types in files importing Avatar/AvatarGroup.',
21
43
  pr: '#2672',
22
44
  };
23
45
 
46
+ /** Import sources that provide the Astryx Avatar/AvatarGroup components. */
24
47
  const IMPORT_SOURCES = new Set([
25
48
  '@astryxdesign/core',
26
49
  '@astryxdesign/core/Avatar',
@@ -30,11 +53,10 @@ const IMPORT_SOURCES = new Set([
30
53
  '@xds/core/AvatarGroup',
31
54
  ]);
32
55
 
33
- const COMPONENT_PROPS = new Map([
34
- ['Avatar', new Set(['size'])],
35
- ['AvatarGroup', new Set(['size'])],
36
- ]);
56
+ /** Component names whose `size` prop is renamed. */
57
+ const TARGET_IMPORTED_NAMES = new Set(['Avatar', 'AvatarGroup']);
37
58
 
59
+ /** Old → new size-name mapping. */
38
60
  const RENAMES = new Map([
39
61
  ['tiny', 'xsm'],
40
62
  ['xsmall', 'sm'],
@@ -43,38 +65,46 @@ const RENAMES = new Map([
43
65
  ['large', 'xl'],
44
66
  ]);
45
67
 
46
- /** @param {string} name */
47
- function canonicalComponentName(name) {
48
- return name.startsWith('XDS') ? name.slice(3) : name;
49
- }
68
+ /**
69
+ * Ambiguous names — common words that must ONLY be renamed in a precise
70
+ * context. `tiny`/`xsmall` are omitted because they are unique to Avatar's
71
+ * scale; their presence in a collection proves it is an Avatar size enum.
72
+ */
73
+ const AMBIGUOUS = new Set(['small', 'medium', 'large']);
74
+
75
+ /** Names that appear only in Avatar's scale — safe signals of Avatar sizing. */
76
+ const UNIQUE = new Set(['tiny', 'xsmall']);
50
77
 
51
- /** @param {any} node @returns {boolean} */
52
- function renameStaticValue(node) {
78
+ /**
79
+ * Rename a string-literal node if its value is an old size name.
80
+ * Unwraps `'small' as const`. In non-precise contexts, ambiguous names are
81
+ * skipped. Returns true when a rename occurred.
82
+ */
83
+ function renameValue(/** @type {any} */ node, {precise = false} = {}) {
53
84
  if (!node) return false;
85
+ const target = node.type === 'TSAsExpression' ? node.expression : node;
86
+ const isString =
87
+ target.type === 'StringLiteral' || target.type === 'Literal';
88
+ if (!isString || typeof target.value !== 'string') return false;
89
+ const replacement = RENAMES.get(target.value);
90
+ if (!replacement) return false;
91
+ if (!precise && AMBIGUOUS.has(target.value)) return false;
92
+ target.value = replacement;
93
+ if (target.raw) target.raw = undefined;
94
+ return true;
95
+ }
96
+
97
+ /** Extract a plain string value from a literal-ish node, or null. */
98
+ function literalString(/** @type {any} */ node) {
99
+ const target = node?.type === 'TSAsExpression' ? node.expression : node;
54
100
  if (
55
- node.type === 'TSAsExpression' ||
56
- node.type === 'TSSatisfiesExpression' ||
57
- node.type === 'TypeCastExpression' ||
58
- node.type === 'ParenthesizedExpression'
59
- ) {
60
- return renameStaticValue(node.expression);
61
- }
62
- if (node.type === 'ConditionalExpression') {
63
- const consequentChanged = renameStaticValue(node.consequent);
64
- const alternateChanged = renameStaticValue(node.alternate);
65
- return consequentChanged || alternateChanged;
66
- }
67
- if (
68
- (node.type !== 'StringLiteral' && node.type !== 'Literal') ||
69
- typeof node.value !== 'string'
101
+ target &&
102
+ (target.type === 'StringLiteral' || target.type === 'Literal') &&
103
+ typeof target.value === 'string'
70
104
  ) {
71
- return false;
105
+ return target.value;
72
106
  }
73
- const replacement = RENAMES.get(node.value);
74
- if (!replacement) return false;
75
- node.value = replacement;
76
- if (node.raw) node.raw = undefined;
77
- return true;
107
+ return null;
78
108
  }
79
109
 
80
110
  /**
@@ -87,23 +117,122 @@ export default function transformer(file, api) {
87
117
  const root = j(file.source);
88
118
  let hasChanges = false;
89
119
 
90
- transformProp(
91
- root,
92
- j,
93
- {
94
- matchesImport: source => IMPORT_SOURCES.has(source),
95
- components: COMPONENT_PROPS,
96
- normalizeComponentName: canonicalComponentName,
97
- },
98
- propPath => {
99
- const prop = propPath.node;
100
- const value =
101
- prop.value?.type === 'JSXExpressionContainer'
102
- ? prop.value.expression
103
- : prop.value;
104
- if (renameStaticValue(value)) hasChanges = true;
105
- },
106
- );
120
+ // Track alias-aware local names for the target components.
121
+ const targetLocals = new Set();
122
+ root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
123
+ if (!IMPORT_SOURCES.has(path.node.source.value)) return;
124
+ for (const spec of path.node.specifiers ?? []) {
125
+ if (
126
+ spec.type === 'ImportSpecifier' &&
127
+ TARGET_IMPORTED_NAMES.has(spec.imported.name)
128
+ ) {
129
+ targetLocals.add(spec.local.name);
130
+ }
131
+ }
132
+ });
133
+
134
+ if (targetLocals.size === 0) return undefined;
135
+
136
+ function renameArrayElements(/** @type {any} */ node) {
137
+ let arr = node;
138
+ if (arr.type === 'TSAsExpression') arr = arr.expression;
139
+ if (arr.type !== 'ArrayExpression') return;
140
+ // If the array contains a name unique to Avatar's scale (tiny/xsmall), the
141
+ // whole collection is unambiguously the Avatar size enum, so it is safe to
142
+ // rename the ambiguous members too. Otherwise stay conservative.
143
+ const precise = arr.elements.some((/** @type {any} */ el) => UNIQUE.has(literalString(el)));
144
+ for (const el of arr.elements) {
145
+ if (renameValue(el, {precise})) hasChanges = true;
146
+ }
147
+ }
148
+
149
+ // 1. `size` JSX attributes on target components — PRECISE (component known),
150
+ // so all five names, including the ambiguous ones, are renamed here.
151
+ root.find(j.JSXOpeningElement).forEach((/** @type {any} */ path) => {
152
+ const name = path.node.name;
153
+ const componentName = name.type === 'JSXIdentifier' ? name.name : null;
154
+ if (!componentName || !targetLocals.has(componentName)) return;
155
+
156
+ for (const attr of path.node.attributes) {
157
+ if (attr.type !== 'JSXAttribute' || attr.name?.name !== 'size') continue;
158
+ const value = attr.value;
159
+
160
+ // size="small"
161
+ if (renameValue(value, {precise: true})) {
162
+ hasChanges = true;
163
+ continue;
164
+ }
165
+ if (
166
+ value &&
167
+ value.type === 'JSXExpressionContainer' &&
168
+ value.expression
169
+ ) {
170
+ // size={'small'}
171
+ if (renameValue(value.expression, {precise: true})) {
172
+ hasChanges = true;
173
+ continue;
174
+ }
175
+ // size={cond ? 'small' : 'large'}
176
+ if (value.expression.type === 'ConditionalExpression') {
177
+ if (renameValue(value.expression.consequent, {precise: true}))
178
+ hasChanges = true;
179
+ if (renameValue(value.expression.alternate, {precise: true}))
180
+ hasChanges = true;
181
+ }
182
+ }
183
+ }
184
+ });
185
+
186
+ // 2. Object properties keyed `size` (or `*Size`) + Storybook `options`
187
+ // arrays — NON-PRECISE (the component isn't known here), so only the
188
+ // unique names tiny/xsmall are renamed.
189
+ const PropertyType = j.ObjectProperty ?? j.Property;
190
+ root.find(PropertyType).forEach((/** @type {any} */ path) => {
191
+ const key = path.node.key;
192
+ const keyName =
193
+ key.type === 'Identifier'
194
+ ? key.name
195
+ : key.type === 'StringLiteral' || key.type === 'Literal'
196
+ ? key.value
197
+ : null;
198
+ if (typeof keyName !== 'string') return;
199
+
200
+ const lower = keyName.toLowerCase();
201
+ if (lower !== 'size' && !lower.endsWith('size')) return;
202
+
203
+ const value = path.node.value;
204
+ // size: 'tiny'
205
+ if (renameValue(value)) {
206
+ hasChanges = true;
207
+ return;
208
+ }
209
+ // size: { control: 'select', options: ['tiny', ...] }
210
+ if (value.type === 'ObjectExpression') {
211
+ const optionsProp = value.properties.find(
212
+ (/** @type {any} */ p) => p.key && (p.key.name === 'options' || p.key.value === 'options'),
213
+ );
214
+ if (optionsProp) renameArrayElements(optionsProp.value);
215
+ }
216
+ // size: ['tiny', 'xsmall', ...]
217
+ renameArrayElements(value);
218
+ });
219
+
220
+ // 3. Union-type literals — NON-PRECISE, so only the unique names tiny/xsmall
221
+ // are renamed. This avoids corrupting unrelated unions that happen to use
222
+ // common words like 'medium' (e.g. a priority or breakpoint type) in a
223
+ // file that also imports Avatar.
224
+ root.find(j.TSLiteralType).forEach((/** @type {any} */ path) => {
225
+ if (renameValue(path.node.literal)) hasChanges = true;
226
+ });
227
+
228
+ // 4. Standalone array literals whose members are exactly the Avatar size
229
+ // scale (identified by the presence of a unique name like tiny/xsmall) —
230
+ // e.g. `(['tiny', 'xsmall', 'small', 'medium', 'large'] as const).map(...)`
231
+ // used to render every size. The unique name proves the whole array is the
232
+ // Avatar enum, so its ambiguous members can be renamed safely.
233
+ root.find(j.ArrayExpression).forEach((/** @type {any} */ path) => {
234
+ renameArrayElements(path.node);
235
+ });
107
236
 
108
237
  if (!hasChanges) return undefined;
109
238
  return root.toSource({quote: 'single'});
@@ -5,7 +5,6 @@ import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import * as os from 'node:os';
7
7
  import {component} from '../../../api/component/component.mjs';
8
- import {runCli} from '../../../test-utils/run-cli.mjs';
9
8
 
10
9
  // These tests create a minimal monorepo fixture with:
11
10
  // - packages/core (symlinked to real @astryxdesign/core for loadDocs compatibility)
@@ -139,31 +138,4 @@ describe('component() with --package option', () => {
139
138
  expect(scoped.type).toBe('component.detail.blocks');
140
139
  expect(scoped.data).toEqual(unscoped.data);
141
140
  });
142
-
143
- // `cwd` is an explicit API input: --blocks must discover from it, as
144
- // --showcase does, not from the process working directory.
145
- it('--blocks discovers blocks from options.cwd, like --showcase', async () => {
146
- const showcase = await component('ProfileCard', {cwd: tmpDir, showcase: true});
147
- expect(showcase.data.source).toContain('ProfileCardShowcase');
148
-
149
- const blocks = await component('ProfileCard', {cwd: tmpDir, blocks: true});
150
- expect(blocks.type).toBe('component.detail.blocks');
151
- expect(blocks.data.showcase?.name).toBe('ProfileCardShowcase');
152
- });
153
-
154
- // List and detail report one import for a legacy package component; the
155
- // text list projects the JSON value instead of deriving a Core path.
156
- it('the names list gives a legacy package component the import its detail reports', async () => {
157
- const detail = await component('ProfileCard', {cwd: tmpDir});
158
- const list = await component(undefined, {cwd: tmpDir, list: true});
159
- const entry = Object.values(list.data.components)
160
- .flat()
161
- .find(e => e.name === 'ProfileCard' && e.package === '@test/ext');
162
- expect(entry?.import).toBe(detail.data.import);
163
-
164
- const text = await runCli(['component', '--list'], tmpDir);
165
- expect(text.code).toBe(0);
166
- expect(text.stdout).toContain(`name: ProfileCard\nimport: ${detail.data.import} [@test/ext]`);
167
- expect(text.stdout).not.toMatch(/^import: +@astryxdesign\/core\S* +\[@test\/ext\]$/m);
168
- });
169
141
  });
@@ -36,7 +36,7 @@ export const doc = {
36
36
  {
37
37
  value: 'component.detail',
38
38
  description:
39
- "One component's authored ComponentDoc plus ownership fields (package, the owner; import, the specifier; sourceAvailable, whether source exists) and parentDoc (present when the component is documented inside another component's doc, naming that doc).",
39
+ "One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available).",
40
40
  },
41
41
  {
42
42
  value: 'component.detail.props',
@@ -71,7 +71,7 @@ export const doc = {
71
71
  {
72
72
  value: 'docs.index',
73
73
  description:
74
- "One topic's section index (--index): the topic's name, title, and description, plus sections, each {id, title, summary} (pass the id as the section argument; summary is the section's one-line summary).",
74
+ "One topic's section index (--index): each section's key, title, and one-line summary.",
75
75
  },
76
76
  {
77
77
  value: 'docs.detail.section',
@@ -116,7 +116,7 @@ export const doc = {
116
116
  {
117
117
  value: 'search',
118
118
  description:
119
- 'The echoed query, `matchCount` (total matches, before `limit`), and results, a ranked SearchResultEntry[] bounded by `limit`: each {domain, name, score, reason, description, command}, plus import (components, hooks), title (docs), or displayName and kind (templates).',
119
+ 'The echoed query, `matchCount` (how many candidates matched in total, before `limit`), plus a ranked SearchResultEntry[] bounded by `limit` (domain, name, score, reason, description, follow-up command, and import path where relevant).',
120
120
  },
121
121
 
122
122
  // build
@@ -128,7 +128,7 @@ export const doc = {
128
128
  {
129
129
  value: 'build.kit',
130
130
  description:
131
- 'The composition kit: echoed query, hasResults, matchCount (total matched, never a cap), directMatch, pages (closest templates), blocks (drop-in patterns) and domain (idea components/hooks) as SearchResultEntry[], frame and foundation name arrays, and hint {reason, commands} when thin.',
131
+ 'The grouped composition kit: echoed query, hasResults/matchCount/directMatch fields (matchCount is the total matched, never a cap read back), the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays.',
132
132
  },
133
133
 
134
134
  // swizzle
@@ -151,7 +151,7 @@ export const doc = {
151
151
  {
152
152
  value: 'gap-report.file',
153
153
  description:
154
- 'An aggregate receipt: overall status, the selected package, issuesUrl (or null), deliveries in handler order, each {handlerType: project | integration | fallback, handler, audience, status, url, message}, and filedCount/routedOnlyCount totals.',
154
+ 'An aggregate receipt with overall status, the selected package and issues URL, ordered per-handler deliveries, and filedCount/routedOnlyCount totals.',
155
155
  },
156
156
 
157
157
  // template
@@ -228,7 +228,7 @@ export const doc = {
228
228
  {
229
229
  value: 'theme.targets',
230
230
  description:
231
- 'The whole themeable surface: the echoed filter, componentCount, and targets, one per theming target — {key, className, component, props, states, deprecatedFor?}, where props and states are its legal override keys and deprecatedFor names the canonical replacement key.',
231
+ 'The whole themeable surface: the echoed filter, the component count, and one entry per theming target — {key, className, component, props, states}, where props and states are its legal override keys.',
232
232
  },
233
233
  {
234
234
  value: 'theme.palette.generate',
@@ -276,7 +276,7 @@ export const doc = {
276
276
  {
277
277
  value: 'integration.pack-check',
278
278
  description:
279
- 'The packed-package check: name, version, packable, tarball {filename, fileCount, size, unpackedSize} or null, inventory {manifest, roots [{kind, path, expectedFiles, missingFiles, complete}], expectedFiles, packedFiles}, contributions {local, packed}, each null or {themes [{slug, exportName}], components, templates [{id, type, name}], codemods [{version, id}], docs, agentDocsAppend}, and issues [{code, severity, message}].',
279
+ 'The packed-package check: package identity, tarball facts, local and packed contribution inventories, and issues.',
280
280
  },
281
281
  {
282
282
  value: 'integration.validate',