@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,161 +1,129 @@
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-avatar-size-scale.mjs';
4
5
 
5
6
  async function applyTransform(source) {
6
- const {default: transform} = await import('../rename-avatar-size-scale.mjs');
7
7
  const jscodeshift = (await import('jscodeshift')).default;
8
8
  const j = jscodeshift.withParser('tsx');
9
9
  const api = {jscodeshift: j, stats: () => {}, report: () => {}};
10
- const file = {source, path: 'test.tsx'};
11
- const result = transform(file, api);
12
- return result ?? source;
10
+ return transform({source, path: 'test.tsx'}, api) ?? source;
13
11
  }
14
12
 
15
13
  describe('rename-avatar-size-scale', () => {
16
- it('renames every named size on an Avatar JSX size attribute', async () => {
17
- const input = `import {Avatar} from '@astryxdesign/core';
14
+ it('renames every direct Avatar size literal', async () => {
15
+ const output =
16
+ await applyTransform(`import {Avatar} from '@astryxdesign/core';
18
17
  const a = <Avatar name="A" size="tiny" />;
19
18
  const b = <Avatar name="B" size="xsmall" />;
20
19
  const c = <Avatar name="C" size="small" />;
21
20
  const d = <Avatar name="D" size="medium" />;
22
- const e = <Avatar name="E" size="large" />;`;
23
- const output = await applyTransform(input);
24
- expect(output).toContain(`size='xsm'`);
25
- expect(output).toContain(`size='sm'`);
26
- expect(output).toContain(`size='md'`);
27
- expect(output).toContain(`size='lg'`);
28
- expect(output).toContain(`size='xl'`);
29
- for (const old of ['tiny', 'xsmall', 'small', 'medium', 'large']) {
30
- expect(output).not.toContain(`size="${old}"`);
21
+ const e = <Avatar name="E" size="large" />;`);
22
+
23
+ for (const size of ['xsm', 'sm', 'md', 'lg', 'xl']) {
24
+ expect(output).toContain(`size='${size}'`);
31
25
  }
32
26
  });
33
27
 
34
- it('renames size on AvatarGroup', async () => {
35
- const input = `import {AvatarGroup} from '@astryxdesign/core';
36
- const x = <AvatarGroup size="medium">{kids}</AvatarGroup>;`;
37
- const output = await applyTransform(input);
38
- expect(output).toContain(`size='lg'`);
39
- });
28
+ it('renames literals inside satisfies expressions while preserving the type', async () => {
29
+ const output =
30
+ await applyTransform(`import {Avatar} from '@astryxdesign/core';
31
+ const view = <Avatar size={'small' satisfies AvatarSize} />;`);
40
32
 
41
- it('leaves numeric sizes untouched', async () => {
42
- const input = `import {Avatar} from '@astryxdesign/core';
43
- const x = <Avatar name="A" size={48} />;`;
44
- const output = await applyTransform(input);
45
- expect(output).toContain('size={48}');
33
+ expect(output).toContain("{'md' satisfies AvatarSize}");
46
34
  });
47
35
 
48
- it('is alias-aware', async () => {
49
- const input = `import {Avatar as Face} from '@astryxdesign/core';
50
- const x = <Face name="A" size="small" />;`;
51
- const output = await applyTransform(input);
52
- expect(output).toContain(`size='md'`);
53
- });
36
+ it('renames AvatarGroup and conditional literals', async () => {
37
+ const output =
38
+ await applyTransform(`import {AvatarGroup} from '@astryxdesign/core';
39
+ const view = <AvatarGroup size={large ? 'large' : 'small'}>{kids}</AvatarGroup>;`);
54
40
 
55
- it('renames the subpath and legacy import sources', async () => {
56
- const sub = `import {Avatar} from '@astryxdesign/core/Avatar';
57
- const x = <Avatar name="A" size="large" />;`;
58
- expect(await applyTransform(sub)).toContain(`size='xl'`);
59
- const legacy = `import {Avatar} from '@xds/core';
60
- const x = <Avatar name="A" size="tiny" />;`;
61
- expect(await applyTransform(legacy)).toContain(`size='xsm'`);
41
+ expect(output).toContain("large ? 'xl' : 'md'");
62
42
  });
63
43
 
64
- it('renames a size inside a ternary on an Avatar element', async () => {
65
- const input = `import {Avatar} from '@astryxdesign/core';
66
- const x = <Avatar name="A" size={big ? 'large' : 'small'} />;`;
67
- const output = await applyTransform(input);
68
- expect(output).toContain(`big ? 'xl' : 'md'`);
69
- });
44
+ it('maps import aliases and default subpath imports', async () => {
45
+ const output =
46
+ await applyTransform(`import {Avatar as Face} from '@xds/core';
47
+ import Faces from '@astryxdesign/core/AvatarGroup';
48
+ const view = <><Face size="medium" /><Faces size="large" /></>;`);
70
49
 
71
- it('renames the UNIQUE names tiny/xsmall in Storybook options and size args', async () => {
72
- const input = `import {Avatar} from '@astryxdesign/core';
73
- const meta = {
74
- argTypes: {size: {control: 'select', options: ['tiny', 'xsmall']}},
75
- args: {size: 'tiny'},
76
- };`;
77
- const output = await applyTransform(input);
78
- expect(output).toContain(`['xsm', 'sm']`);
79
- expect(output).toContain(`size: 'xsm'`);
50
+ expect(output).toContain("<Face size='lg'");
51
+ expect(output).toContain("<Faces size='xl'");
80
52
  });
81
53
 
82
- it('renames a FULL Storybook options array (unique name unlocks ambiguous members)', async () => {
83
- // The presence of a unique name (tiny/xsmall) proves the whole array is the
84
- // Avatar size enum, so small/medium/large in it are safe to rename too.
85
- const input = `import {Avatar} from '@astryxdesign/core';
86
- const meta = {
87
- argTypes: {size: {control: 'select', options: ['tiny', 'xsmall', 'small', 'medium', 'large']}},
88
- };`;
89
- const output = await applyTransform(input);
90
- expect(output).toContain(`['xsm', 'sm', 'md', 'lg', 'xl']`);
91
- });
54
+ it('ignores declaration-level type imports paired with a local Avatar', async () => {
55
+ const input = `import type {Avatar} from '@astryxdesign/core';
56
+ const Avatar = props => <div {...props} />;
57
+ const view = <Avatar size="small" />;`;
92
58
 
93
- it('renames a standalone Avatar-size array literal used with .map()', async () => {
94
- const input = `import {AvatarGroup} from '@astryxdesign/core';
95
- const sizes = (['tiny', 'xsmall', 'small', 'medium', 'large'] as const).map(s => s);`;
96
- const output = await applyTransform(input);
97
- expect(output).toContain(`['xsm', 'sm', 'md', 'lg', 'xl']`);
59
+ expect(await applyTransform(input)).toBe(input);
98
60
  });
99
61
 
100
- it('does NOT rename an array of ambiguous words with no unique Avatar name', async () => {
101
- // Without tiny/xsmall, the array could be anything (priorities, densities),
102
- // so ambiguous members are left untouched.
103
- const input = `import {Avatar} from '@astryxdesign/core';
104
- const densities = ['small', 'medium', 'large'] as const;`;
105
- const output = await applyTransform(input);
106
- expect(output).toContain(`['small', 'medium', 'large']`);
107
- });
62
+ it('ignores mixed type specifiers while migrating runtime imports', async () => {
63
+ const output =
64
+ await applyTransform(`import {type Avatar, AvatarGroup} from '@astryxdesign/core';
65
+ const Avatar = props => <div {...props} />;
66
+ const view = <><Avatar size="small" /><AvatarGroup size="large" /></>;`);
108
67
 
109
- it('renames a UNIQUE name in a size-typed union literal', async () => {
110
- const input = `import {Avatar} from '@astryxdesign/core';
111
- type Props = {size: 'tiny' | 'xsmall'};`;
112
- const output = await applyTransform(input);
113
- expect(output).toContain(`'xsm' | 'sm'`);
68
+ expect(output).toContain('<Avatar size="small"');
69
+ expect(output).toContain("<AvatarGroup size='xl'");
114
70
  });
115
71
 
116
- // --- Precision guards: ambiguous common words must NOT be corrupted in
117
- // --- context-blind positions, even in files that import Avatar. ---
72
+ it('maps namespace imports', async () => {
73
+ const output =
74
+ await applyTransform(`import * as Core from '@astryxdesign/core';
75
+ const view = <Core.Avatar size="small" />;`);
118
76
 
119
- it('does NOT rename ambiguous names in an unrelated union type', async () => {
120
- const input = `import {Avatar} from '@astryxdesign/core';
121
- type TaskPriority = 'urgent' | 'high' | 'medium' | 'low' | 'none';`;
122
- const output = await applyTransform(input);
123
- expect(output).toContain(`'medium'`);
124
- expect(output).not.toContain(`'lg'`);
77
+ expect(output).toContain("size='md'");
125
78
  });
126
79
 
127
- it('does NOT rename an ambiguous name in a non-size object property', async () => {
80
+ it('leaves numeric and dynamic sizes untouched', async () => {
128
81
  const input = `import {Avatar} from '@astryxdesign/core';
129
- const cfg = {priority: 'medium', density: 'large'};`;
130
- const output = await applyTransform(input);
131
- expect(output).toContain(`priority: 'medium'`);
132
- expect(output).toContain(`density: 'large'`);
82
+ const named = 'small';
83
+ const a = <Avatar size={48} />;
84
+ const b = <Avatar size={named} />;`;
85
+ expect(await applyTransform(input)).toBe(input);
133
86
  });
134
87
 
135
- it('does NOT rename an ambiguous name in a size-keyed object property (component unknown)', async () => {
136
- // A `size: 'small'` object entry could belong to any component; renaming it
137
- // blind would be unsafe, so it is left for manual migration.
88
+ it('leaves objects, types, arrays, and Storybook metadata untouched', async () => {
138
89
  const input = `import {Avatar} from '@astryxdesign/core';
139
- const args = {size: 'small'};`;
140
- const output = await applyTransform(input);
141
- expect(output).toContain(`size: 'small'`);
90
+ type Density = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large';
91
+ const card = {size: 'small'};
92
+ const sizes = ['tiny', 'xsmall', 'small', 'medium', 'large'];
93
+ const meta = {argTypes: {size: {options: sizes}}};
94
+ const view = <Avatar size={card.size} />;`;
95
+
96
+ expect(await applyTransform(input)).toBe(input);
142
97
  });
143
98
 
144
- it('does not touch files that never import Avatar/AvatarGroup', async () => {
99
+ it('does not touch files without an Avatar import', async () => {
145
100
  const input = `import {Badge} from '@astryxdesign/core';
146
- const x = <Badge size="small" />;
147
- type P = 'tiny' | 'xsmall';`;
148
- const output = await applyTransform(input);
149
- expect(output).toContain(`size="small"`);
150
- expect(output).toContain(`'tiny' | 'xsmall'`);
101
+ const view = <Badge size="small" />;`;
102
+ expect(await applyTransform(input)).toBe(input);
151
103
  });
152
104
 
153
- it('does not rename an unrelated non-size string in an Avatar file', async () => {
154
- const input = `import {Avatar} from '@astryxdesign/core';
155
- const label = 'small';
156
- const x = <Avatar name="A" size="small" />;`;
157
- const output = await applyTransform(input);
158
- expect(output).toContain(`const label = 'small'`);
159
- expect(output).toContain(`size='md'`);
105
+ it('ignores same-named components imported from other packages', async () => {
106
+ const input = `import {Avatar} from '@other/core';
107
+ const view = <Avatar size="small" />;`;
108
+ expect(await applyTransform(input)).toBe(input);
109
+ });
110
+
111
+ it('ignores a local component that shadows an imported alias', async () => {
112
+ const output =
113
+ await applyTransform(`import {Avatar as Face} from '@xds/core';
114
+ const migrated = <Face size="medium" />;
115
+ function Local(Face) { return <Face size="large" />; }`);
116
+
117
+ expect(output).toContain("size='lg'");
118
+ expect(output).toContain('size="large"');
119
+ });
120
+
121
+ it('leaves unrelated props on Avatar untouched', async () => {
122
+ const output =
123
+ await applyTransform(`import {Avatar} from '@astryxdesign/core';
124
+ const view = <Avatar size="small" label="small" />;`);
125
+
126
+ expect(output).toContain("size='md'");
127
+ expect(output).toContain('label="small"');
160
128
  });
161
129
  });
@@ -4,46 +4,23 @@
4
4
  * @file Codemod: Rename Avatar named sizes to Icon's abbreviated scale
5
5
  * @see https://github.com/facebook/astryx/issues/2672
6
6
  *
7
- * As of v0.1.8, Avatar and AvatarGroup use the same abbreviated size scale as
8
- * Icon (`xsm`/`sm`/`md`/`lg`/`xl`) instead of full words. Pixel values are
9
- * unchanged — only the names move:
10
- *
11
- * tiny (20px) → xsm
12
- * xsmall (24px) → sm
13
- * small (36px) → md ← also the new default (was `small`)
14
- * medium (48px) → lg
15
- * large (128px) → xl
16
- *
17
- * The default size shifts from `small` to `md`, but that is the SAME 36px, so
18
- * call sites that relied on the default need no change.
19
- *
20
- * ## Precision
21
- *
22
- * `small`, `medium`, and `large` are common English words that appear as
23
- * unrelated string/type literals (priorities, statuses, breakpoints, …). To
24
- * avoid corrupting those, the three AMBIGUOUS names are renamed ONLY in a
25
- * precise context: a `size` JSX attribute on a component that resolves to an
26
- * Avatar/AvatarGroup import (alias-aware).
27
- *
28
- * `tiny` and `xsmall` are unique to Avatar's scale, so they are additionally
29
- * renamed in context-blind positions (object properties keyed `size`/`*Size`,
30
- * Storybook `options` arrays, and size-typed union literals) within files that
31
- * import Avatar/AvatarGroup.
7
+ * This transform is deliberately limited to static literals written directly
8
+ * in the `size` prop of an imported Avatar or AvatarGroup JSX element. It does
9
+ * not infer through variables, helpers, objects, types, Storybook metadata, or
10
+ * wrappers: missing an indirect migration is safer than rewriting another
11
+ * component's size vocabulary.
32
12
  */
33
13
 
14
+ import {transformProp} from '../../transform-prop.mjs';
15
+
34
16
  export const meta = {
35
17
  title: 'Rename Avatar named sizes tiny/xsmall/small/medium/large → xsm/sm/md/lg/xl',
36
18
  description:
37
- 'Avatar and AvatarGroup adopt Icon\'s abbreviated size scale ' +
38
- '(xsm/sm/md/lg/xl). Pixel values are unchanged; the default moves from ' +
39
- '`small` to `md` (both 36px). The ambiguous names small/medium/large are ' +
40
- 'only renamed on Avatar/AvatarGroup `size` JSX attributes; the unique ' +
41
- 'names tiny/xsmall are also renamed in size-keyed props, Storybook ' +
42
- 'options, and union types in files importing Avatar/AvatarGroup.',
19
+ 'Renames static sizes written directly on imported Avatar and AvatarGroup ' +
20
+ 'JSX elements. Indirect values are left unchanged for manual migration.',
43
21
  pr: '#2672',
44
22
  };
45
23
 
46
- /** Import sources that provide the Astryx Avatar/AvatarGroup components. */
47
24
  const IMPORT_SOURCES = new Set([
48
25
  '@astryxdesign/core',
49
26
  '@astryxdesign/core/Avatar',
@@ -53,10 +30,11 @@ const IMPORT_SOURCES = new Set([
53
30
  '@xds/core/AvatarGroup',
54
31
  ]);
55
32
 
56
- /** Component names whose `size` prop is renamed. */
57
- const TARGET_IMPORTED_NAMES = new Set(['Avatar', 'AvatarGroup']);
33
+ const COMPONENT_PROPS = new Map([
34
+ ['Avatar', new Set(['size'])],
35
+ ['AvatarGroup', new Set(['size'])],
36
+ ]);
58
37
 
59
- /** Old → new size-name mapping. */
60
38
  const RENAMES = new Map([
61
39
  ['tiny', 'xsm'],
62
40
  ['xsmall', 'sm'],
@@ -65,46 +43,38 @@ const RENAMES = new Map([
65
43
  ['large', 'xl'],
66
44
  ]);
67
45
 
68
- /**
69
- * Ambiguous names — common words that must ONLY be renamed in a precise
70
- * context. `tiny`/`xsmall` are omitted because they are unique to Avatar's
71
- * scale; their presence in a collection proves it is an Avatar size enum.
72
- */
73
- const AMBIGUOUS = new Set(['small', 'medium', 'large']);
74
-
75
- /** Names that appear only in Avatar's scale — safe signals of Avatar sizing. */
76
- const UNIQUE = new Set(['tiny', 'xsmall']);
77
-
78
- /**
79
- * Rename a string-literal node if its value is an old size name.
80
- * Unwraps `'small' as const`. In non-precise contexts, ambiguous names are
81
- * skipped. Returns true when a rename occurred.
82
- */
83
- function renameValue(/** @type {any} */ node, {precise = false} = {}) {
84
- if (!node) return false;
85
- const target = node.type === 'TSAsExpression' ? node.expression : node;
86
- const isString =
87
- target.type === 'StringLiteral' || target.type === 'Literal';
88
- if (!isString || typeof target.value !== 'string') return false;
89
- const replacement = RENAMES.get(target.value);
90
- if (!replacement) return false;
91
- if (!precise && AMBIGUOUS.has(target.value)) return false;
92
- target.value = replacement;
93
- if (target.raw) target.raw = undefined;
94
- return true;
46
+ /** @param {string} name */
47
+ function canonicalComponentName(name) {
48
+ return name.startsWith('XDS') ? name.slice(3) : name;
95
49
  }
96
50
 
97
- /** Extract a plain string value from a literal-ish node, or null. */
98
- function literalString(/** @type {any} */ node) {
99
- const target = node?.type === 'TSAsExpression' ? node.expression : node;
51
+ /** @param {any} node @returns {boolean} */
52
+ function renameStaticValue(node) {
53
+ if (!node) return false;
100
54
  if (
101
- target &&
102
- (target.type === 'StringLiteral' || target.type === 'Literal') &&
103
- typeof target.value === 'string'
55
+ node.type === 'TSAsExpression' ||
56
+ node.type === 'TSSatisfiesExpression' ||
57
+ node.type === 'TypeCastExpression' ||
58
+ node.type === 'ParenthesizedExpression'
104
59
  ) {
105
- return target.value;
60
+ return renameStaticValue(node.expression);
106
61
  }
107
- return null;
62
+ if (node.type === 'ConditionalExpression') {
63
+ const consequentChanged = renameStaticValue(node.consequent);
64
+ const alternateChanged = renameStaticValue(node.alternate);
65
+ return consequentChanged || alternateChanged;
66
+ }
67
+ if (
68
+ (node.type !== 'StringLiteral' && node.type !== 'Literal') ||
69
+ typeof node.value !== 'string'
70
+ ) {
71
+ return false;
72
+ }
73
+ const replacement = RENAMES.get(node.value);
74
+ if (!replacement) return false;
75
+ node.value = replacement;
76
+ if (node.raw) node.raw = undefined;
77
+ return true;
108
78
  }
109
79
 
110
80
  /**
@@ -117,122 +87,23 @@ export default function transformer(file, api) {
117
87
  const root = j(file.source);
118
88
  let hasChanges = false;
119
89
 
120
- // Track alias-aware local names for the target components.
121
- const targetLocals = new Set();
122
- root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
123
- if (!IMPORT_SOURCES.has(path.node.source.value)) return;
124
- for (const spec of path.node.specifiers ?? []) {
125
- if (
126
- spec.type === 'ImportSpecifier' &&
127
- TARGET_IMPORTED_NAMES.has(spec.imported.name)
128
- ) {
129
- targetLocals.add(spec.local.name);
130
- }
131
- }
132
- });
133
-
134
- if (targetLocals.size === 0) return undefined;
135
-
136
- function renameArrayElements(/** @type {any} */ node) {
137
- let arr = node;
138
- if (arr.type === 'TSAsExpression') arr = arr.expression;
139
- if (arr.type !== 'ArrayExpression') return;
140
- // If the array contains a name unique to Avatar's scale (tiny/xsmall), the
141
- // whole collection is unambiguously the Avatar size enum, so it is safe to
142
- // rename the ambiguous members too. Otherwise stay conservative.
143
- const precise = arr.elements.some((/** @type {any} */ el) => UNIQUE.has(literalString(el)));
144
- for (const el of arr.elements) {
145
- if (renameValue(el, {precise})) hasChanges = true;
146
- }
147
- }
148
-
149
- // 1. `size` JSX attributes on target components — PRECISE (component known),
150
- // so all five names, including the ambiguous ones, are renamed here.
151
- root.find(j.JSXOpeningElement).forEach((/** @type {any} */ path) => {
152
- const name = path.node.name;
153
- const componentName = name.type === 'JSXIdentifier' ? name.name : null;
154
- if (!componentName || !targetLocals.has(componentName)) return;
155
-
156
- for (const attr of path.node.attributes) {
157
- if (attr.type !== 'JSXAttribute' || attr.name?.name !== 'size') continue;
158
- const value = attr.value;
159
-
160
- // size="small"
161
- if (renameValue(value, {precise: true})) {
162
- hasChanges = true;
163
- continue;
164
- }
165
- if (
166
- value &&
167
- value.type === 'JSXExpressionContainer' &&
168
- value.expression
169
- ) {
170
- // size={'small'}
171
- if (renameValue(value.expression, {precise: true})) {
172
- hasChanges = true;
173
- continue;
174
- }
175
- // size={cond ? 'small' : 'large'}
176
- if (value.expression.type === 'ConditionalExpression') {
177
- if (renameValue(value.expression.consequent, {precise: true}))
178
- hasChanges = true;
179
- if (renameValue(value.expression.alternate, {precise: true}))
180
- hasChanges = true;
181
- }
182
- }
183
- }
184
- });
185
-
186
- // 2. Object properties keyed `size` (or `*Size`) + Storybook `options`
187
- // arrays — NON-PRECISE (the component isn't known here), so only the
188
- // unique names tiny/xsmall are renamed.
189
- const PropertyType = j.ObjectProperty ?? j.Property;
190
- root.find(PropertyType).forEach((/** @type {any} */ path) => {
191
- const key = path.node.key;
192
- const keyName =
193
- key.type === 'Identifier'
194
- ? key.name
195
- : key.type === 'StringLiteral' || key.type === 'Literal'
196
- ? key.value
197
- : null;
198
- if (typeof keyName !== 'string') return;
199
-
200
- const lower = keyName.toLowerCase();
201
- if (lower !== 'size' && !lower.endsWith('size')) return;
202
-
203
- const value = path.node.value;
204
- // size: 'tiny'
205
- if (renameValue(value)) {
206
- hasChanges = true;
207
- return;
208
- }
209
- // size: { control: 'select', options: ['tiny', ...] }
210
- if (value.type === 'ObjectExpression') {
211
- const optionsProp = value.properties.find(
212
- (/** @type {any} */ p) => p.key && (p.key.name === 'options' || p.key.value === 'options'),
213
- );
214
- if (optionsProp) renameArrayElements(optionsProp.value);
215
- }
216
- // size: ['tiny', 'xsmall', ...]
217
- renameArrayElements(value);
218
- });
219
-
220
- // 3. Union-type literals — NON-PRECISE, so only the unique names tiny/xsmall
221
- // are renamed. This avoids corrupting unrelated unions that happen to use
222
- // common words like 'medium' (e.g. a priority or breakpoint type) in a
223
- // file that also imports Avatar.
224
- root.find(j.TSLiteralType).forEach((/** @type {any} */ path) => {
225
- if (renameValue(path.node.literal)) hasChanges = true;
226
- });
227
-
228
- // 4. Standalone array literals whose members are exactly the Avatar size
229
- // scale (identified by the presence of a unique name like tiny/xsmall) —
230
- // e.g. `(['tiny', 'xsmall', 'small', 'medium', 'large'] as const).map(...)`
231
- // used to render every size. The unique name proves the whole array is the
232
- // Avatar enum, so its ambiguous members can be renamed safely.
233
- root.find(j.ArrayExpression).forEach((/** @type {any} */ path) => {
234
- renameArrayElements(path.node);
235
- });
90
+ transformProp(
91
+ root,
92
+ j,
93
+ {
94
+ matchesImport: source => IMPORT_SOURCES.has(source),
95
+ components: COMPONENT_PROPS,
96
+ normalizeComponentName: canonicalComponentName,
97
+ },
98
+ propPath => {
99
+ const prop = propPath.node;
100
+ const value =
101
+ prop.value?.type === 'JSXExpressionContainer'
102
+ ? prop.value.expression
103
+ : prop.value;
104
+ if (renameStaticValue(value)) hasChanges = true;
105
+ },
106
+ );
236
107
 
237
108
  if (!hasChanges) return undefined;
238
109
  return root.toSource({quote: 'single'});
@@ -220,4 +220,51 @@ function wrapper(useResizable) {
220
220
  const output = await apply('rename-resizable-pixel-bounds', input);
221
221
  expect(output).toBe(input);
222
222
  });
223
+
224
+ it('renames bounds when useResizable comes from a namespace import', async () => {
225
+ const input = `import * as Astryx from '@astryxdesign/core';
226
+ const region = Astryx.useResizable({defaultSize: 240, minSizePx: 120, maxSizePx: 480});`;
227
+ const output = await apply('rename-resizable-pixel-bounds', input);
228
+ expect(output).toContain('minSize: 120');
229
+ expect(output).toContain('maxSize: 480');
230
+ expect(output).not.toContain('minSizePx');
231
+ expect(output).not.toContain('maxSizePx');
232
+ });
233
+
234
+ it('leaves a namespace that does not come from Astryx alone', async () => {
235
+ const input = `import * as Other from 'other-resizable';
236
+ const region = Other.useResizable({minSizePx: 120});`;
237
+ const output = await apply('rename-resizable-pixel-bounds', input);
238
+ expect(output).toContain('minSizePx: 120');
239
+ });
240
+
241
+ it('does not rewrite a parameter that shadows the namespace import', async () => {
242
+ const input = `import * as Astryx from '@astryxdesign/core';
243
+ function build(Astryx) {
244
+ return Astryx.useResizable({minSizePx: 120});
245
+ }`;
246
+ const output = await apply('rename-resizable-pixel-bounds', input);
247
+ expect(output).toContain('minSizePx: 120');
248
+ });
249
+
250
+ it.each([
251
+ ['as const', '{defaultSize: 240, minSizePx: 120} as const'],
252
+ ['satisfies', '{defaultSize: 240, minSizePx: 120} satisfies object'],
253
+ ])('renames a static configuration wrapped in %s', async (_label, config) => {
254
+ const input = `import {useResizable} from '@astryxdesign/core/Resizable';
255
+ const region = useResizable(${config});`;
256
+ const output = await apply('rename-resizable-pixel-bounds', input);
257
+ expect(output).toContain('minSize: 120');
258
+ expect(output).not.toContain('minSizePx');
259
+ });
260
+
261
+ it('renames wrapped multi-region configurations', async () => {
262
+ const input = `import {useResizable} from '@astryxdesign/core';
263
+ const regions = useResizable({regions: {
264
+ nav: {defaultSize: 240, minSizePx: 120} as const,
265
+ } as const});`;
266
+ const output = await apply('rename-resizable-pixel-bounds', input);
267
+ expect(output).toContain('minSize: 120');
268
+ expect(output).not.toContain('minSizePx');
269
+ });
223
270
  });