@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,126 +1,205 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- import {describe, expect, it} from 'vitest';
4
- import transform from '../rename-status-variants.mjs';
3
+ import {describe, it, expect} from 'vitest';
5
4
 
6
5
  async function applyTransform(source) {
6
+ const {default: transform} = await import(
7
+ '../rename-status-variants.mjs'
8
+ );
7
9
  const jscodeshift = (await import('jscodeshift')).default;
8
10
  const j = jscodeshift.withParser('tsx');
9
11
  const api = {jscodeshift: j, stats: () => {}, report: () => {}};
10
- return transform({source, path: 'test.tsx'}, api) ?? source;
12
+ const file = {source, path: 'test.tsx'};
13
+ const result = transform(file, api);
14
+ return result ?? source;
11
15
  }
12
16
 
13
17
  describe('rename-status-variants', () => {
14
- it('renames direct StatusDot literals and ternary branches', async () => {
15
- const output = await applyTransform(`import {StatusDot} from '@astryxdesign/core';
16
- const a = <StatusDot variant="positive" />;
17
- const b = <StatusDot variant={'negative'} />;
18
- const c = <StatusDot variant={ready ? 'info' : 'positive'} />;`);
19
-
20
- expect(output).toContain("variant='success'");
21
- expect(output).toContain("variant={'error'}");
22
- expect(output).toContain("ready ? 'accent' : 'success'");
18
+ it('renames StatusDot variant="positive" to "success"', async () => {
19
+ const input = `<XDSStatusDot variant="positive" label="Online" />`;
20
+ const output = await applyTransform(input);
21
+ expect(output).toContain("'success'");
22
+ expect(output).not.toContain("'positive'");
23
23
  });
24
24
 
25
- it('renames literals inside satisfies expressions while preserving the type', async () => {
26
- const output =
27
- await applyTransform(`import {StatusDot} from '@astryxdesign/core';
28
- const view = <StatusDot variant={'positive' satisfies StatusDotVariant} />;`);
29
-
30
- expect(output).toContain("{'success' satisfies StatusDotVariant}");
25
+ it('renames StatusDot variant="negative" to "error"', async () => {
26
+ const input = `<XDSStatusDot variant="negative" label="Offline" />`;
27
+ const output = await applyTransform(input);
28
+ expect(output).toContain("'error'");
29
+ expect(output).not.toContain("'negative'");
31
30
  });
32
31
 
33
- it('renames Icon, SVGIcon, AvatarStatusDot, and ProgressBar props', async () => {
34
- const output = await applyTransform(`import {
35
- Icon,
36
- SVGIcon,
37
- AvatarStatusDot,
38
- ProgressBar,
39
- } from '@astryxdesign/core';
40
- const view = <>
41
- <Icon color="negative" />
42
- <SVGIcon color="positive" />
43
- <AvatarStatusDot variant="negative" />
44
- <ProgressBar variant="positive" />
45
- </>;`);
46
-
47
- expect(output.match(/'error'/g)).toHaveLength(2);
48
- expect(output.match(/'success'/g)).toHaveLength(2);
32
+ it('renames StatusDot variant="info" to "accent"', async () => {
33
+ const input = `<XDSStatusDot variant="info" label="Info" />`;
34
+ const output = await applyTransform(input);
35
+ expect(output).toContain("'accent'");
36
+ expect(output).not.toContain("'info'");
49
37
  });
50
38
 
51
- it('maps import aliases and default subpath imports', async () => {
52
- const output = await applyTransform(`import {StatusDot as Dot} from '@xds/core';
53
- import IconGlyph from '@astryxdesign/core/Icon';
54
- const view = <><Dot variant="positive" /><IconGlyph color="negative" /></>;`);
39
+ it('renames AvatarStatusDot variant="positive" to "success"', async () => {
40
+ const input = `<XDSAvatarStatusDot variant="positive" label="Online" />`;
41
+ const output = await applyTransform(input);
42
+ expect(output).toContain("'success'");
43
+ expect(output).not.toContain("'positive'");
44
+ });
55
45
 
56
- expect(output).toContain("<Dot variant='success'");
57
- expect(output).toContain("<IconGlyph color='error'");
46
+ it('renames Icon color="positive" to "success"', async () => {
47
+ const input = `<XDSIcon color="positive" icon={CheckIcon} />`;
48
+ const output = await applyTransform(input);
49
+ expect(output).toContain("'success'");
50
+ expect(output).not.toContain("'positive'");
58
51
  });
59
52
 
60
- it('maps namespace imports', async () => {
61
- const output = await applyTransform(`import * as Core from '@astryxdesign/core';
62
- const view = <Core.StatusDot variant="info" />;`);
53
+ it('renames Icon color="negative" to "error"', async () => {
54
+ const input = `<XDSIcon color="negative" icon={XIcon} />`;
55
+ const output = await applyTransform(input);
56
+ expect(output).toContain("'error'");
57
+ expect(output).not.toContain("'negative'");
58
+ });
63
59
 
64
- expect(output).toContain("variant='accent'");
60
+ it('renames ProgressBar variant="positive" to "success"', async () => {
61
+ const input = `<XDSProgressBar variant="positive" value={50} />`;
62
+ const output = await applyTransform(input);
63
+ expect(output).toContain("'success'");
64
+ expect(output).not.toContain("'positive'");
65
65
  });
66
66
 
67
- it('keeps Badge info distinct while renaming its removed variants', async () => {
68
- const output = await applyTransform(`import {Badge} from '@astryxdesign/core';
69
- const view = <>
70
- <Badge variant="info" />
71
- <Badge variant="positive" />
72
- <Badge variant="negative" />
73
- </>;`);
67
+ it('renames ProgressBar variant="negative" to "error"', async () => {
68
+ const input = `<XDSProgressBar variant="negative" value={50} />`;
69
+ const output = await applyTransform(input);
70
+ expect(output).toContain("'error'");
71
+ expect(output).not.toContain("'negative'");
72
+ });
74
73
 
75
- expect(output).toContain('variant="info"');
76
- expect(output).toContain("variant='success'");
77
- expect(output).toContain("variant='error'");
74
+ it('handles expression container: variant={"positive"}', async () => {
75
+ const input = `<XDSStatusDot variant={'positive'} label="Online" />`;
76
+ const output = await applyTransform(input);
77
+ expect(output).toContain("'success'");
78
+ expect(output).not.toContain("'positive'");
78
79
  });
79
80
 
80
- it('leaves variables, helpers, objects, types, and Storybook metadata untouched', async () => {
81
- const input = `import {StatusDot} from '@astryxdesign/core';
82
- type CellTone = 'positive' | 'negative' | 'info';
83
- const args = {variant: 'positive'};
84
- const options = ['positive', 'negative', 'info'];
85
- function tone() { return 'negative' as const; }
86
- const view = <StatusDot variant={tone()} />;`;
81
+ it('handles ternary: variant={x ? "positive" : "negative"}', async () => {
82
+ const input = `<XDSStatusDot variant={isOnline ? 'positive' : 'negative'} label="Status" />`;
83
+ const output = await applyTransform(input);
84
+ expect(output).toContain("'success'");
85
+ expect(output).toContain("'error'");
86
+ expect(output).not.toContain("'positive'");
87
+ expect(output).not.toContain("'negative'");
88
+ });
87
89
 
88
- expect(await applyTransform(input)).toBe(input);
90
+ it('renames object property values in files importing target components', async () => {
91
+ const input = `import { XDSStatusDot } from '@xds/core/StatusDot';
92
+ const args = { variant: 'positive' };`;
93
+ const output = await applyTransform(input);
94
+ expect(output).toContain("'success'");
95
+ expect(output).not.toContain("'positive'");
89
96
  });
90
97
 
91
- it('leaves unrelated props untouched', async () => {
92
- const output = await applyTransform(`import {StatusDot} from '@astryxdesign/core';
93
- const view = <StatusDot variant="positive" label="positive" />;`);
98
+ it('renames unambiguous values in Storybook argType options arrays but leaves ambiguous "info"', async () => {
99
+ const input = `import { XDSStatusDot } from '@xds/core/StatusDot';
100
+ const meta = { argTypes: { variant: { options: ['positive', 'negative', 'warning', 'info', 'neutral'] } } };`;
101
+ const output = await applyTransform(input);
102
+ expect(output).toContain("'success'");
103
+ expect(output).toContain("'error'");
104
+ expect(output).not.toContain("'positive'");
105
+ expect(output).not.toContain("'negative'");
106
+ // 'info' is ambiguous (valid on Badge etc.) and this is a context-blind
107
+ // path — the codemod cannot tell which component the options describe, so
108
+ // it is left for human review rather than risk rewriting a valid value.
109
+ expect(output).toContain("'info'");
110
+ expect(output).not.toContain("'accent'");
111
+ // warning and neutral are unchanged
112
+ expect(output).toContain("'warning'");
113
+ expect(output).toContain("'neutral'");
114
+ });
94
115
 
95
- expect(output).toContain("variant='success'");
96
- expect(output).toContain('label="positive"');
116
+ it('does not touch unrelated components', async () => {
117
+ const input = `<XDSBadge variant="info" label="New" />`;
118
+ const output = await applyTransform(input);
119
+ // XDSBadge is not in the target list
120
+ expect(output).toContain('info');
97
121
  });
98
122
 
99
- it('ignores same-named components imported from other packages', async () => {
100
- const input = `import {StatusDot} from '@other/core';
101
- const view = <StatusDot variant="positive" />;`;
102
- expect(await applyTransform(input)).toBe(input);
123
+ it('does not rewrite Badge config "info" in a file that also imports a target (Icon)', async () => {
124
+ // Regression: a file may import a target component (e.g. XDSIcon) AND
125
+ // declare a typed config object for a NON-target component (Badge, whose
126
+ // BadgeVariantMap still has `info`). The context-blind object-property path
127
+ // must NOT rewrite that `info` to `accent` just because the file imports a
128
+ // target — `accent` is not a valid Badge variant. positive/negative stay
129
+ // renamed (no component keeps them), but info is preserved.
130
+ const input = `import { XDSIcon } from '@xds/core/Icon';
131
+ import { XDSBadge, type XDSBadgeVariant } from '@xds/core/Badge';
132
+ const PROGRESS_CONFIG: Record<string, {label: string; variant: XDSBadgeVariant}> = {
133
+ OPEN: {label: 'Open', variant: 'info'},
134
+ DONE: {label: 'Done', variant: 'positive'},
135
+ BLOCKED: {label: 'Blocked', variant: 'negative'},
136
+ };`;
137
+ const output = await applyTransform(input);
138
+ // Ambiguous 'info' on a Badge config is left intact.
139
+ expect(output).toContain("variant: 'info'");
140
+ expect(output).not.toContain("'accent'");
141
+ // Unambiguous values are still migrated (no component retains them).
142
+ expect(output).toContain("variant: 'success'");
143
+ expect(output).toContain("variant: 'error'");
144
+ expect(output).not.toContain("'positive'");
145
+ expect(output).not.toContain("'negative'");
103
146
  });
104
147
 
105
- it('ignores a local component that shadows an imported alias', async () => {
106
- const output = await applyTransform(`import {StatusDot as Dot} from '@xds/core';
107
- const migrated = <Dot variant="positive" />;
108
- function Local(Dot) { return <Dot variant="negative" />; }`);
148
+ it('still renames "info" to "accent" on a direct target JSX attribute', async () => {
149
+ // The precise path (component is known) keeps the full mapping, including
150
+ // the ambiguous info -> accent, even when other components are imported.
151
+ const input = `import { XDSStatusDot } from '@xds/core/StatusDot';
152
+ import { XDSBadge } from '@xds/core/Badge';
153
+ const a = <XDSStatusDot variant="info" label="Status" />;
154
+ const b = <XDSBadge variant="info" label="New" />;`;
155
+ const output = await applyTransform(input);
156
+ // StatusDot's info -> accent (precise), Badge's info stays.
157
+ expect(output).toContain("<XDSStatusDot variant='accent'");
158
+ expect(output).toContain('<XDSBadge variant="info"');
159
+ });
109
160
 
110
- expect(output).toContain("variant='success'");
111
- expect(output).toContain('variant="negative"');
161
+ it('does not touch unrelated props on target components', async () => {
162
+ const input = `<XDSStatusDot variant="positive" label="positive result" />`;
163
+ const output = await applyTransform(input);
164
+ expect(output).toContain("'success'");
165
+ // The label "positive result" should NOT be renamed
166
+ expect(output).toContain('positive result');
112
167
  });
113
168
 
114
- it('returns undefined when no direct prop needs a change', async () => {
169
+ it('returns undefined when no changes needed', async () => {
170
+ const {default: transform} = await import(
171
+ '../rename-status-variants.mjs'
172
+ );
115
173
  const jscodeshift = (await import('jscodeshift')).default;
116
174
  const j = jscodeshift.withParser('tsx');
117
- const source = `import {StatusDot} from '@astryxdesign/core';
118
- const view = <StatusDot variant="success" />;`;
119
- expect(
120
- transform(
121
- {source, path: 'test.tsx'},
122
- {jscodeshift: j, stats: () => {}, report: () => {}},
123
- ),
124
- ).toBeUndefined();
175
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
176
+ const source = `<XDSStatusDot variant='success' label='Online' />`;
177
+ const result = transform({source, path: 'test.tsx'}, api);
178
+ expect(result).toBeUndefined();
179
+ });
180
+
181
+ it('handles a full component file', async () => {
182
+ const input = `import { XDSStatusDot } from '@xds/core/StatusDot';
183
+ import { XDSIcon } from '@xds/core/Icon';
184
+
185
+ function StatusIndicator({ isOnline }: { isOnline: boolean }) {
186
+ return (
187
+ <div>
188
+ <XDSStatusDot
189
+ variant={isOnline ? 'positive' : 'negative'}
190
+ label={isOnline ? 'Online' : 'Offline'}
191
+ />
192
+ <XDSIcon
193
+ icon={isOnline ? CheckIcon : XIcon}
194
+ color={isOnline ? 'positive' : 'negative'}
195
+ />
196
+ </div>
197
+ );
198
+ }`;
199
+ const output = await applyTransform(input);
200
+ expect(output).toContain("'success'");
201
+ expect(output).toContain("'error'");
202
+ expect(output).not.toContain("'positive'");
203
+ expect(output).not.toContain("'negative'");
125
204
  });
126
205
  });
@@ -4,81 +4,86 @@
4
4
  * @file Codemod: Rename status variants positive/negative → success/error
5
5
  * @see https://github.com/facebookexperimental/xds/issues/996
6
6
  *
7
- * This transform is deliberately limited to static literals written directly
8
- * in affected props on imported JSX components. It does not infer through
9
- * variables, helpers, objects, types, or wrappers: missing an indirect
10
- * migration is safer than rewriting an unrelated domain using the same words.
7
+ * Converges component APIs to match the token layer naming:
8
+ * - `positive` → `success` (maps to --color-success)
9
+ * - `negative` → `error` (maps to --color-error)
10
+ * - `info` → `accent` (maps to --color-accent)
11
+ *
12
+ * Affected components:
13
+ * - XDSStatusDot: variant="positive|negative|info" → variant="success|error|accent"
14
+ * - XDSAvatarStatusDot: variant="positive|negative" → variant="success|error"
15
+ * - XDSIcon: color="positive|negative" → color="success|error"
16
+ * - XDSProgressBar: variant="positive|negative" → variant="success|error"
17
+ *
18
+ * Transforms JSX attributes, object properties (in files importing affected
19
+ * components), TypeScript type annotations, and Storybook argType options.
11
20
  */
12
21
 
13
- import {transformProp} from '../../transform-prop.mjs';
14
-
15
22
  export const meta = {
16
23
  title: 'Rename status variants positive/negative → success/error',
17
24
  description:
18
- 'Renames static status values written directly on imported StatusDot, ' +
19
- 'AvatarStatusDot, Icon, SVGIcon, ProgressBar, and Badge JSX elements. ' +
20
- 'Indirect values are left unchanged for manual migration.',
25
+ 'Renames `positive` to `success`, `negative` to `error`, and `info` to `accent` ' +
26
+ 'on StatusDot, AvatarStatusDot, Icon, SVGIcon, and ProgressBar to align with token naming. ' +
27
+ 'The `info` -> `accent` rename is only applied to direct JSX attributes on these ' +
28
+ 'components (where the component is known); in context-blind locations (object ' +
29
+ 'properties, type unions) `info` is left untouched because it is a valid variant on ' +
30
+ 'other components such as Badge.',
21
31
  pr: '#996',
22
32
  };
23
33
 
24
- const CORE_IMPORT_RE = /^@(astryxdesign|xds)\/core(?:\/|$)/;
25
-
26
- const COMPONENT_PROPS = new Map([
27
- ['StatusDot', new Set(['variant'])],
28
- ['AvatarStatusDot', new Set(['variant'])],
29
- ['Icon', new Set(['color'])],
30
- ['SVGIcon', new Set(['color'])],
31
- ['ProgressBar', new Set(['variant'])],
32
- ['Badge', new Set(['variant'])],
33
- ]);
34
-
35
- const STATUS_RENAMES = new Map([
34
+ /** Rename mappings. */
35
+ const RENAMES = new Map([
36
36
  ['positive', 'success'],
37
37
  ['negative', 'error'],
38
38
  ['info', 'accent'],
39
39
  ]);
40
40
 
41
- const BADGE_RENAMES = new Map([
42
- ['positive', 'success'],
43
- ['negative', 'error'],
41
+ /**
42
+ * Ambiguous renames that are only safe when the target component is known.
43
+ *
44
+ * `info` is a VALID variant on non-target components (notably XDSBadge, whose
45
+ * BadgeVariantMap still has `info`). `positive`/`negative` are not retained by
46
+ * any component post-migration, so renaming them is always safe. Therefore the
47
+ * context-blind heuristic paths (object properties, type unions, and suffixed
48
+ * props on wrapper components) must NOT rewrite `info` — only the precise path,
49
+ * a direct JSX attribute on a known target component, may. Otherwise a
50
+ * `variant: 'info'` on an unrelated Badge config in a file that merely also
51
+ * imports e.g. XDSIcon gets wrongly rewritten to the invalid `accent`.
52
+ */
53
+ const AMBIGUOUS_VALUES = new Set(['info']);
54
+
55
+
56
+ /**
57
+ * Components and which props are affected.
58
+ * Key: component name. Value: set of prop names to check.
59
+ */
60
+ const COMPONENT_PROPS = new Map([
61
+ ['XDSStatusDot', new Set(['variant'])],
62
+ ['XDSAvatarStatusDot', new Set(['variant'])],
63
+ ['XDSIcon', new Set(['color'])],
64
+ ['XDSProgressBar', new Set(['variant'])],
44
65
  ]);
45
66
 
46
- /** @param {string} name */
47
- function canonicalComponentName(name) {
48
- return name.startsWith('XDS') ? name.slice(3) : name;
67
+ /** All affected component names for quick lookup. */
68
+ const TARGET_COMPONENTS = new Set(COMPONENT_PROPS.keys());
69
+
70
+ /** All affected prop names (variant, color). */
71
+ const TARGET_PROP_NAMES = new Set();
72
+ for (const props of COMPONENT_PROPS.values()) {
73
+ for (const p of props) TARGET_PROP_NAMES.add(p);
49
74
  }
50
75
 
51
76
  /**
52
- * @param {any} node
53
- * @param {ReadonlyMap<string, string>} renames
54
- * @returns {boolean}
77
+ * Check if a key matches a target prop — exact or case-insensitive suffix.
78
+ * e.g. "dotVariant" matches because it ends with "variant".
55
79
  */
56
- function renameStaticValue(node, renames) {
57
- if (!node) return false;
58
- if (
59
- node.type === 'TSAsExpression' ||
60
- node.type === 'TSSatisfiesExpression' ||
61
- node.type === 'TypeCastExpression' ||
62
- node.type === 'ParenthesizedExpression'
63
- ) {
64
- return renameStaticValue(node.expression, renames);
80
+ function matchesPropName(/** @type {any} */ keyName) {
81
+ if (TARGET_PROP_NAMES.has(keyName)) return true;
82
+ const lower = keyName.toLowerCase();
83
+ for (const prop of TARGET_PROP_NAMES) {
84
+ if (lower.endsWith(prop.toLowerCase()) && lower !== prop.toLowerCase()) return true;
65
85
  }
66
- if (node.type === 'ConditionalExpression') {
67
- const consequentChanged = renameStaticValue(node.consequent, renames);
68
- const alternateChanged = renameStaticValue(node.alternate, renames);
69
- return consequentChanged || alternateChanged;
70
- }
71
- if (
72
- (node.type !== 'StringLiteral' && node.type !== 'Literal') ||
73
- typeof node.value !== 'string'
74
- ) {
75
- return false;
76
- }
77
- const replacement = renames.get(node.value);
78
- if (!replacement) return false;
79
- node.value = replacement;
80
- if (node.raw) node.raw = undefined;
81
- return true;
86
+ return false;
82
87
  }
83
88
 
84
89
  /**
@@ -91,25 +96,158 @@ export default function transformer(file, api) {
91
96
  const root = j(file.source);
92
97
  let hasChanges = false;
93
98
 
94
- transformProp(
95
- root,
96
- j,
97
- {
98
- matchesImport: source => CORE_IMPORT_RE.test(source),
99
- components: COMPONENT_PROPS,
100
- normalizeComponentName: canonicalComponentName,
101
- },
102
- (propPath, {component}) => {
103
- const prop = propPath.node;
104
- const value =
105
- prop.value?.type === 'JSXExpressionContainer'
106
- ? prop.value.expression
107
- : prop.value;
108
- const renames = component === 'Badge' ? BADGE_RENAMES : STATUS_RENAMES;
109
- if (renameStaticValue(value, renames)) hasChanges = true;
110
- },
99
+ /**
100
+ * Rename a string literal value if it matches our renames.
101
+ * Unwraps TSAsExpression (e.g. 'positive' as const) to reach the literal.
102
+ * @returns {boolean} whether a rename occurred
103
+ */
104
+ function renameValue(/** @type {any} */ node, {precise = false} = {}) {
105
+ if (!node) return false;
106
+
107
+ // Unwrap TSAsExpression (e.g. 'positive' as const → StringLiteral)
108
+ const target = node.type === 'TSAsExpression' ? node.expression : node;
109
+
110
+ const isString =
111
+ target.type === 'StringLiteral' || target.type === 'Literal';
112
+ if (!isString || typeof target.value !== 'string') return false;
113
+
114
+ const replacement = RENAMES.get(target.value);
115
+ if (!replacement) return false;
116
+
117
+ // Ambiguous values (e.g. `info`) are only renamed in a precise context
118
+ // where the target component is known (a direct JSX attribute on a target).
119
+ // In context-blind paths they may belong to a component that legitimately
120
+ // keeps the value (e.g. Badge's `info`), so leave them untouched.
121
+ if (!precise && AMBIGUOUS_VALUES.has(target.value)) return false;
122
+
123
+ target.value = replacement;
124
+ if (target.raw) target.raw = undefined;
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Rename elements in an array (handles TSAsExpression wrapping).
130
+ */
131
+ function renameArrayElements(/** @type {any} */ node, {precise = false} = {}) {
132
+ let arrayNode = node;
133
+ if (arrayNode.type === 'TSAsExpression') arrayNode = arrayNode.expression;
134
+ if (arrayNode.type !== 'ArrayExpression') return;
135
+ arrayNode.elements.forEach((/** @type {any} */ el) => {
136
+ if (renameValue(el, {precise})) hasChanges = true;
137
+ });
138
+ }
139
+
140
+ // Determine early: does this file import any target component?
141
+ const importsTarget = [...TARGET_COMPONENTS].some(
142
+ (name) =>
143
+ root.find(j.ImportSpecifier, {imported: {name}}).length > 0,
111
144
  );
112
145
 
146
+ // 1. JSX attributes on target components
147
+ root.find(j.JSXOpeningElement).forEach((/** @type {any} */ path) => {
148
+ const name = path.node.name;
149
+ const componentName = name.type === 'JSXIdentifier' ? name.name : null;
150
+ if (!componentName) return;
151
+
152
+ const isDirectTarget = TARGET_COMPONENTS.has(componentName);
153
+ // For non-target components, only check suffixed props if file imports targets
154
+ if (!isDirectTarget && !importsTarget) return;
155
+
156
+ path.node.attributes.forEach((/** @type {any} */ attr) => {
157
+ if (attr.type !== 'JSXAttribute') return;
158
+ const attrName = attr.name.name;
159
+
160
+ if (isDirectTarget) {
161
+ // Direct targets: only their specific props
162
+ const targetProps = COMPONENT_PROPS.get(componentName);
163
+ if (!targetProps || !targetProps.has(attrName)) return;
164
+ } else {
165
+ // Non-target components: suffixed prop names only
166
+ // (e.g. dotVariant on ChangelogSection wrapping XDSStatusDot)
167
+ if (!matchesPropName(attrName) || TARGET_PROP_NAMES.has(attrName)) return;
168
+ }
169
+
170
+ // Only a direct JSX attribute on a target component is a precise context
171
+ // where the component (and thus the validity of `info`) is known. The
172
+ // wrapper/suffixed-prop case is a heuristic and must not touch ambiguous
173
+ // values.
174
+ const precise = isDirectTarget;
175
+ const value = attr.value;
176
+
177
+ // variant="positive" (string literal)
178
+ if (renameValue(value, {precise})) {
179
+ hasChanges = true;
180
+ return;
181
+ }
182
+
183
+ // variant={'positive'} (expression container)
184
+ if (
185
+ value &&
186
+ value.type === 'JSXExpressionContainer' &&
187
+ value.expression
188
+ ) {
189
+ if (renameValue(value.expression, {precise})) {
190
+ hasChanges = true;
191
+ return;
192
+ }
193
+
194
+ // variant={condition ? 'positive' : 'negative'} (ternary)
195
+ if (value.expression.type === 'ConditionalExpression') {
196
+ if (renameValue(value.expression.consequent, {precise}))
197
+ hasChanges = true;
198
+ if (renameValue(value.expression.alternate, {precise}))
199
+ hasChanges = true;
200
+ }
201
+ }
202
+ });
203
+ });
204
+
205
+ // 2. Object properties in files importing target components.
206
+ // Handles args objects, config objects, Storybook argTypes, etc.
207
+ if (importsTarget) {
208
+ const PropertyType = j.ObjectProperty ?? j.Property;
209
+ root.find(PropertyType).forEach((/** @type {any} */ path) => {
210
+ const key = path.node.key;
211
+ const keyName =
212
+ key.type === 'Identifier'
213
+ ? key.name
214
+ : key.type === 'StringLiteral' || key.type === 'Literal'
215
+ ? key.value
216
+ : null;
217
+
218
+ if (!keyName || !matchesPropName(keyName)) return;
219
+
220
+ const value = path.node.value;
221
+
222
+ // variant: 'positive' → variant: 'success'
223
+ if (renameValue(value)) {
224
+ hasChanges = true;
225
+ return;
226
+ }
227
+
228
+ // variant: { options: ['positive', 'negative', ...] }
229
+ if (value.type === 'ObjectExpression') {
230
+ const optionsProp = value.properties.find(
231
+ (/** @type {any} */ p) => p.key && (p.key.name === 'options' || p.key.value === 'options'),
232
+ );
233
+ if (optionsProp) {
234
+ renameArrayElements(optionsProp.value);
235
+ }
236
+ }
237
+
238
+ // variant: ['positive', 'negative'] or [...] as Type[]
239
+ renameArrayElements(value);
240
+ });
241
+ }
242
+
243
+ // 3. TypeScript type references: 'positive' | 'negative' in union types
244
+ if (importsTarget) {
245
+ root.find(j.TSLiteralType).forEach((/** @type {any} */ path) => {
246
+ const lit = path.node.literal;
247
+ if (renameValue(lit)) hasChanges = true;
248
+ });
249
+ }
250
+
113
251
  if (!hasChanges) return undefined;
114
252
  return root.toSource({quote: 'single'});
115
253
  }