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

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 (206) hide show
  1. package/README.md +86 -72
  2. package/api/build/build.doc.mjs +3 -3
  3. package/api/build/build.test.mjs +14 -3
  4. package/api/build/build.type.d.mts +35 -1
  5. package/api/build/build.type.mjs +24 -3
  6. package/api/build/help/help.d.mts +8 -5
  7. package/api/build/help/help.mjs +56 -6
  8. package/api/component/component.doc.mjs +12 -3
  9. package/api/component/component.mjs +31 -2
  10. package/api/component/component.test.mjs +38 -0
  11. package/api/component/component.type.d.mts +16 -5
  12. package/api/component/component.type.mjs +13 -5
  13. package/api/component/detail/blocks/blocks.d.mts +2 -1
  14. package/api/component/detail/blocks/blocks.mjs +4 -3
  15. package/api/component/list/list.d.mts +0 -5
  16. package/api/component/list/list.mjs +37 -9
  17. package/api/discover/discover.doc.mjs +1 -1
  18. package/api/gap-report/gap-report.doc.mjs +7 -4
  19. package/api/init/init.doc.mjs +4 -0
  20. package/api/init/init.test.mjs +41 -1
  21. package/api/init/remove/remove.mjs +1 -1
  22. package/api/init/run/run.mjs +20 -10
  23. package/api/integration/add-contribution.component-names.test.mjs +120 -0
  24. package/api/integration/add-contribution.mjs +11 -0
  25. package/api/integration/add-contribution.test.mjs +80 -0
  26. package/api/integration/integrationAdd.doc.mjs +7 -4
  27. package/api/integration/integrationAddComponent.doc.mjs +1 -1
  28. package/api/integration/validate-integration.mjs +2 -0
  29. package/api/integration/validate-integration.test.mjs +91 -0
  30. package/api/integration/validateIntegration.doc.mjs +1 -1
  31. package/api/json/envelope-types.test.mjs +76 -0
  32. package/api/json/parseResponse.doc.mjs +2 -2
  33. package/api/layout/expand/expand.mjs +7 -5
  34. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  35. package/api/layout/grammar/grammar.mjs +2 -1
  36. package/api/layout/layoutExpand.doc.mjs +1 -1
  37. package/api/search/search-return-type.test.mjs +54 -0
  38. package/api/search/search.d.mts +2 -9
  39. package/api/search/search.doc.mjs +5 -1
  40. package/api/search/search.mjs +10 -3
  41. package/api/search/search.test.mjs +34 -0
  42. package/api/swizzle/copy/copy.mjs +28 -11
  43. package/api/swizzle/swizzle.doc.mjs +1 -1
  44. package/api/template/copy/copy.mjs +17 -23
  45. package/api/template/copy/copy.test.mjs +17 -0
  46. package/api/template/template.doc.mjs +2 -1
  47. package/api/theme/add/add.binary.test.mjs +91 -0
  48. package/api/theme/add/add.mjs +20 -3
  49. package/api/theme/add/add.staging.test.mjs +66 -0
  50. package/api/theme/build/build.mjs +114 -37
  51. package/api/theme/build/build.public-component-vars.test.mjs +1 -1
  52. package/api/theme/build/build.receipt-doc.test.mjs +111 -0
  53. package/api/theme/build/font-warning.mjs +3 -3
  54. package/api/theme/build/font-warning.test.mjs +5 -2
  55. package/api/theme/palette/generate/generate.mjs +7 -2
  56. package/api/theme/palette/generate/generate.test.mjs +96 -0
  57. package/api/theme/palette/generate/generator.mjs +8 -1
  58. package/api/theme/palette/generate/generator.test.mjs +10 -0
  59. package/api/theme/template/template.mjs +11 -2
  60. package/api/theme/template/template.test.mjs +15 -0
  61. package/api/theme/themeBuild.doc.mjs +7 -4
  62. package/api/theme/themeTemplate.doc.mjs +4 -1
  63. package/api/upgrade/_adapter.mjs +27 -4
  64. package/api/upgrade/list/list.mjs +2 -1
  65. package/api/upgrade/list/list.test.mjs +73 -0
  66. package/api/upgrade/status/status.mjs +2 -2
  67. package/api/upgrade/upgrade.doc.mjs +2 -1
  68. package/api/upgrade/upgrade.type.d.mts +4 -0
  69. package/api/upgrade/upgrade.type.mjs +1 -0
  70. package/assets/codemods/term-log.mjs +32 -8
  71. package/assets/codemods/term-log.test.mjs +19 -1
  72. package/assets/codemods/transform-prop.mjs +109 -0
  73. package/assets/codemods/transform-prop.test.mjs +95 -0
  74. package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +86 -165
  75. package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +72 -210
  76. package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +83 -115
  77. package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +57 -186
  78. package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
  79. package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
  80. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +14 -0
  81. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +61 -0
  82. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +14 -0
  83. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +126 -0
  84. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +15 -0
  85. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +86 -0
  86. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +14 -0
  87. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +99 -0
  88. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +14 -0
  89. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +122 -0
  90. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +14 -0
  91. package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +95 -0
  92. package/assets/templates/pages/table-tree/page.tsx +1704 -0
  93. package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
  94. package/authoring/config/config.doc.mjs +1 -1
  95. package/authoring/config/debug-composition.test.mjs +92 -0
  96. package/authoring/config/type.ts +15 -3
  97. package/clients/cli/command-load-failure.test.mjs +83 -0
  98. package/clients/cli/commands/blog.mjs +23 -8
  99. package/clients/cli/commands/blog.test.mjs +42 -1
  100. package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
  101. package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
  102. package/clients/cli/commands/build-theme.mjs +5 -5
  103. package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
  104. package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
  105. package/clients/cli/commands/build.doc.mjs +4 -1
  106. package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
  107. package/clients/cli/commands/build.mjs +32 -47
  108. package/clients/cli/commands/build.playbook.test.mjs +75 -0
  109. package/clients/cli/commands/build.text-fields.test.mjs +40 -0
  110. package/clients/cli/commands/component/index.mjs +2 -7
  111. package/clients/cli/commands/component-package.test.mjs +46 -0
  112. package/clients/cli/commands/component-resolution.test.mjs +21 -0
  113. package/clients/cli/commands/component.test.mjs +19 -0
  114. package/clients/cli/commands/detail-levels.test.mjs +2 -2
  115. package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
  116. package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
  117. package/clients/cli/commands/discover.doc.mjs +4 -2
  118. package/clients/cli/commands/discover.mjs +3 -3
  119. package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
  120. package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
  121. package/clients/cli/commands/doctor.mjs +6 -16
  122. package/clients/cli/commands/doctor.test.mjs +42 -0
  123. package/clients/cli/commands/gap-report.doc.mjs +16 -5
  124. package/clients/cli/commands/gap-report.test.mjs +72 -0
  125. package/clients/cli/commands/hook/index.mjs +7 -17
  126. package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
  127. package/clients/cli/commands/init.doc.mjs +19 -8
  128. package/clients/cli/commands/init.flag-help.test.mjs +153 -0
  129. package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
  130. package/clients/cli/commands/integration-add.doc.mjs +21 -5
  131. package/clients/cli/commands/interactive-guard.test.mjs +101 -24
  132. package/clients/cli/commands/json-contract.test.mjs +33 -0
  133. package/clients/cli/commands/layout-check.doc.mjs +14 -3
  134. package/clients/cli/commands/layout-expand.doc.mjs +21 -4
  135. package/clients/cli/commands/layout.doc.mjs +2 -2
  136. package/clients/cli/commands/layout.mjs +21 -9
  137. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  138. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  139. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  140. package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
  141. package/clients/cli/commands/search.doc.mjs +6 -3
  142. package/clients/cli/commands/search.mjs +17 -7
  143. package/clients/cli/commands/search.test.mjs +75 -0
  144. package/clients/cli/commands/swizzle.doc.mjs +2 -1
  145. package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
  146. package/clients/cli/commands/template.doc.mjs +28 -6
  147. package/clients/cli/commands/template.flag-help.test.mjs +117 -0
  148. package/clients/cli/commands/template.path-help.test.mjs +40 -0
  149. package/clients/cli/commands/theme-add.doc.mjs +2 -1
  150. package/clients/cli/commands/theme-build.doc.mjs +7 -6
  151. package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
  152. package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
  153. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  154. package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
  155. package/clients/cli/commands/upgrade.doc.mjs +17 -6
  156. package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
  157. package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
  158. package/clients/cli/index.mjs +13 -30
  159. package/clients/cli/latest-version-env.test.mjs +50 -0
  160. package/clients/cli/lib/cli-error.test.mjs +7 -0
  161. package/clients/cli/lib/component-format.mjs +9 -9
  162. package/clients/cli/lib/component-format.test.mjs +1 -1
  163. package/clients/cli/lib/define-command.mjs +32 -6
  164. package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
  165. package/clients/cli/lib/exit-codes.test.mjs +97 -0
  166. package/clients/cli/lib/hook-format.mjs +5 -5
  167. package/clients/cli/lib/json-shim.mjs +38 -2
  168. package/clients/cli/lib/json-shim.test.mjs +83 -0
  169. package/clients/cli/lib/manifest.d.ts +2 -0
  170. package/clients/cli/lib/manifest.mjs +22 -0
  171. package/clients/cli/lib/manifest.test.mjs +17 -0
  172. package/foundation/agent-docs/agent-docs.d.mts +4 -0
  173. package/foundation/agent-docs/agent-docs.mjs +69 -3
  174. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
  175. package/foundation/config/integration-debug.test.mjs +28 -3
  176. package/foundation/config/project.mjs +123 -9
  177. package/foundation/config/project.test.mjs +126 -0
  178. package/foundation/discovery/template-adapter.d.mts +14 -5
  179. package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
  180. package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
  181. package/foundation/discovery/template-adapter.mjs +292 -25
  182. package/foundation/discovery/theming-targets.test.mjs +4 -0
  183. package/foundation/fs/module-loader.d.mts +1 -0
  184. package/foundation/fs/module-loader.mjs +50 -1
  185. package/foundation/fs/module-loader.stdout.test.mjs +332 -0
  186. package/foundation/fs/path-safety.d.mts +3 -2
  187. package/foundation/fs/path-safety.mjs +49 -19
  188. package/foundation/fs/path-safety.test.mjs +50 -0
  189. package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
  190. package/foundation/integrations/autolink.mjs +6 -1
  191. package/foundation/response/base.d.ts +8 -4
  192. package/foundation/response/error-codes.doc.mjs +1 -2
  193. package/foundation/response/error-codes.mjs +1 -1
  194. package/foundation/response/error-codes.test.mjs +83 -0
  195. package/foundation/response/json-contract.test.mjs +11 -0
  196. package/foundation/response/json.d.mts +4 -2
  197. package/foundation/response/json.mjs +8 -10
  198. package/foundation/response/response-types.doc.mjs +10 -10
  199. package/foundation/response/response-types.doc.test.mjs +158 -0
  200. package/foundation/xle/expand.d.mts +2 -0
  201. package/foundation/xle/expand.mjs +3 -2
  202. package/foundation/xle/expand.test.mjs +54 -0
  203. package/package.json +9 -9
  204. package/clients/cli/lib/update-check.mjs +0 -83
  205. package/clients/cli/lib/update-check.test.mjs +0 -137
  206. package/clients/cli/update-hint-commands.test.mjs +0 -54
@@ -1,205 +1,126 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- import {describe, it, expect} from 'vitest';
3
+ import {describe, expect, it} from 'vitest';
4
+ import transform from '../rename-status-variants.mjs';
4
5
 
5
6
  async function applyTransform(source) {
6
- const {default: transform} = await import(
7
- '../rename-status-variants.mjs'
8
- );
9
7
  const jscodeshift = (await import('jscodeshift')).default;
10
8
  const j = jscodeshift.withParser('tsx');
11
9
  const api = {jscodeshift: j, stats: () => {}, report: () => {}};
12
- const file = {source, path: 'test.tsx'};
13
- const result = transform(file, api);
14
- return result ?? source;
10
+ return transform({source, path: 'test.tsx'}, api) ?? source;
15
11
  }
16
12
 
17
13
  describe('rename-status-variants', () => {
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
- });
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'} />;`);
24
19
 
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'");
20
+ expect(output).toContain("variant='success'");
21
+ expect(output).toContain("variant={'error'}");
22
+ expect(output).toContain("ready ? 'accent' : 'success'");
30
23
  });
31
24
 
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'");
37
- });
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} />;`);
38
29
 
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'");
30
+ expect(output).toContain("{'success' satisfies StatusDotVariant}");
44
31
  });
45
32
 
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'");
51
- });
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
+ </>;`);
52
46
 
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'");
47
+ expect(output.match(/'error'/g)).toHaveLength(2);
48
+ expect(output.match(/'success'/g)).toHaveLength(2);
58
49
  });
59
50
 
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
- });
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" /></>;`);
66
55
 
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'");
56
+ expect(output).toContain("<Dot variant='success'");
57
+ expect(output).toContain("<IconGlyph color='error'");
72
58
  });
73
59
 
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'");
79
- });
60
+ it('maps namespace imports', async () => {
61
+ const output = await applyTransform(`import * as Core from '@astryxdesign/core';
62
+ const view = <Core.StatusDot variant="info" />;`);
80
63
 
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'");
64
+ expect(output).toContain("variant='accent'");
88
65
  });
89
66
 
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'");
96
- });
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
+ </>;`);
97
74
 
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'");
75
+ expect(output).toContain('variant="info"');
76
+ expect(output).toContain("variant='success'");
77
+ expect(output).toContain("variant='error'");
114
78
  });
115
79
 
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');
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()} />;`;
87
+
88
+ expect(await applyTransform(input)).toBe(input);
121
89
  });
122
90
 
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'");
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" />;`);
94
+
95
+ expect(output).toContain("variant='success'");
96
+ expect(output).toContain('label="positive"');
146
97
  });
147
98
 
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"');
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);
159
103
  });
160
104
 
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');
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" />; }`);
109
+
110
+ expect(output).toContain("variant='success'");
111
+ expect(output).toContain('variant="negative"');
167
112
  });
168
113
 
169
- it('returns undefined when no changes needed', async () => {
170
- const {default: transform} = await import(
171
- '../rename-status-variants.mjs'
172
- );
114
+ it('returns undefined when no direct prop needs a change', async () => {
173
115
  const jscodeshift = (await import('jscodeshift')).default;
174
116
  const j = jscodeshift.withParser('tsx');
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'");
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();
204
125
  });
205
126
  });
@@ -4,86 +4,81 @@
4
4
  * @file Codemod: Rename status variants positive/negative → success/error
5
5
  * @see https://github.com/facebookexperimental/xds/issues/996
6
6
  *
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.
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.
20
11
  */
21
12
 
13
+ import {transformProp} from '../../transform-prop.mjs';
14
+
22
15
  export const meta = {
23
16
  title: 'Rename status variants positive/negative → success/error',
24
17
  description:
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.',
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.',
31
21
  pr: '#996',
32
22
  };
33
23
 
34
- /** Rename mappings. */
35
- const RENAMES = new Map([
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([
36
36
  ['positive', 'success'],
37
37
  ['negative', 'error'],
38
38
  ['info', 'accent'],
39
39
  ]);
40
40
 
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'])],
41
+ const BADGE_RENAMES = new Map([
42
+ ['positive', 'success'],
43
+ ['negative', 'error'],
65
44
  ]);
66
45
 
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);
46
+ /** @param {string} name */
47
+ function canonicalComponentName(name) {
48
+ return name.startsWith('XDS') ? name.slice(3) : name;
74
49
  }
75
50
 
76
51
  /**
77
- * Check if a key matches a target prop — exact or case-insensitive suffix.
78
- * e.g. "dotVariant" matches because it ends with "variant".
52
+ * @param {any} node
53
+ * @param {ReadonlyMap<string, string>} renames
54
+ * @returns {boolean}
79
55
  */
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;
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);
85
65
  }
86
- return false;
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;
87
82
  }
88
83
 
89
84
  /**
@@ -96,158 +91,25 @@ export default function transformer(file, api) {
96
91
  const root = j(file.source);
97
92
  let hasChanges = false;
98
93
 
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,
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
+ },
144
111
  );
145
112
 
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
-
251
113
  if (!hasChanges) return undefined;
252
114
  return root.toSource({quote: 'single'});
253
115
  }