@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.
- package/README.md +57 -57
- package/api/component/component.doc.mjs +1 -1
- package/api/component/component.mjs +3 -3
- package/api/component/component.type.d.mts +4 -5
- package/api/component/component.type.mjs +4 -5
- package/api/component/detail/blocks/blocks.d.mts +1 -2
- package/api/component/detail/blocks/blocks.mjs +3 -4
- package/api/component/list/list.d.mts +5 -0
- package/api/component/list/list.mjs +9 -37
- package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +165 -86
- package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +210 -72
- package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +115 -83
- package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +186 -57
- package/clients/cli/commands/component-package.test.mjs +0 -28
- package/foundation/response/response-types.doc.mjs +7 -7
- package/package.json +9 -9
- package/assets/codemods/transform-prop.mjs +0 -109
- package/assets/codemods/transform-prop.test.mjs +0 -95
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +0 -61
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +0 -126
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +0 -15
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +0 -86
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +0 -99
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +0 -122
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +0 -95
- 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,
|
|
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
|
-
|
|
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
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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('
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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('
|
|
61
|
-
const
|
|
62
|
-
const
|
|
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
|
-
|
|
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('
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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('
|
|
81
|
-
const input =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
92
|
-
const
|
|
93
|
-
const
|
|
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
|
-
|
|
96
|
-
|
|
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('
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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('
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
19
|
-
'AvatarStatusDot, Icon, SVGIcon, ProgressBar
|
|
20
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
53
|
-
*
|
|
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
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
}
|