@astryxdesign/cli 0.1.7 → 0.1.8-canary.08a0957
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/CHANGELOG.md +40 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +1 -1
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +1 -1
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +1 -1
- package/docs/tokens.doc.mjs +1 -1
- package/docs/working-with-ai.doc.mjs +19 -19
- package/package.json +11 -10
- package/scripts/postinstall.mjs +74 -0
- package/src/api/doctor.mjs +3 -3
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
- package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
- package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
- package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
- package/src/commands/agent-docs.mjs +136 -20
- package/src/commands/agent-docs.test.mjs +123 -10
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/cli-postinstall.test.mjs +42 -0
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +7 -5
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +48 -152
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +19 -22
- package/src/commands/json-contract.test.mjs +2 -2
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/setup-nudge.test.mjs +108 -0
- package/src/commands/swizzle.mjs +11 -34
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +138 -32
- package/src/commands/upgrade.test.mjs +155 -1
- package/src/index.mjs +40 -6
- package/src/lib/component-format.mjs +2 -1
- package/src/lib/term-log.mjs +48 -0
- package/src/types/template-api.d.ts +4 -1
- package/src/types/upgrade.d.ts +39 -2
- package/src/utils/package-manager.mjs +78 -0
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +2 -1
- package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
- package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
- package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
- package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
- package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
- package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockTerminal.tsx +1 -1
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
- package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
- package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
- package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/templates/blocks/components/Table/TableRowStatusTable.doc.mjs +14 -0
- package/templates/blocks/components/Table/TableRowStatusTable.tsx +59 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
- package/templates/pages/ai-chat/page.tsx +4 -4
- package/templates/pages/dashboard-portfolio/page.tsx +3 -11
- package/templates/pages/detail-page/page.tsx +5 -12
- package/templates/pages/documentation-design/page.tsx +1 -1
- package/templates/pages/messaging-shell/page.tsx +6 -8
- package/templates/pages/table-grouped/page.tsx +9 -14
- package/templates/pages/table-page/page.tsx +7 -9
- package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
- package/docs/integration-authoring.md +0 -105
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
|
@@ -35,11 +35,19 @@
|
|
|
35
35
|
* including type references in generic type-argument positions such as
|
|
36
36
|
* `useMemo<XDSTableColumn<Issue>[]>(...)`
|
|
37
37
|
* 3. Re-exports from @xds/core: export {XDSButton} from '@xds/core/Button'
|
|
38
|
+
* 4. Object-property keys inside a test-framework mock factory for an
|
|
39
|
+
* `@xds/core` module: the partial-mock override in
|
|
40
|
+
* `vi.mock('@xds/core/Text', async orig => ({...await orig(),
|
|
41
|
+
* useXDSTruncation: () => ...}))` must be un-prefixed to `useTruncation`,
|
|
42
|
+
* otherwise the key targets a name the renamed module no longer exports and
|
|
43
|
+
* the mock silently overrides nothing.
|
|
38
44
|
*
|
|
39
45
|
* Does NOT touch:
|
|
40
46
|
* - import source paths (`@xds/core/Button` stays -- subpath dirs are unchanged
|
|
41
47
|
* by the prefix migration)
|
|
42
48
|
* - identifiers not bound to an @xds/core import
|
|
49
|
+
* - object-property keys outside an `@xds/core` mock factory (an unrelated
|
|
50
|
+
* `{useXDSFoo: ...}` literal, or a mock of some other package, is untouched)
|
|
43
51
|
* - the `XDSBaseProps`-style names only when they are NOT imported from core
|
|
44
52
|
*/
|
|
45
53
|
|
|
@@ -48,7 +56,8 @@ export const meta = {
|
|
|
48
56
|
description:
|
|
49
57
|
'Rewrites @xds/core imports and their usages from the prefixed names ' +
|
|
50
58
|
'(XDSButton, useXDSTheme, XDSIconRegistry, XDSButtonProps) to their bare ' +
|
|
51
|
-
'names (Button, useTheme, IconRegistry, ButtonProps)
|
|
59
|
+
'names (Button, useTheme, IconRegistry, ButtonProps), including override ' +
|
|
60
|
+
'keys in a vi.mock/jest.mock factory for an @xds/core module. Only renames ' +
|
|
52
61
|
'bindings imported from @xds/core, leaving unrelated identifiers untouched.',
|
|
53
62
|
pr: '#2880',
|
|
54
63
|
};
|
|
@@ -74,6 +83,81 @@ function bareName(name) {
|
|
|
74
83
|
return null;
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
// A test-framework mock of an `@xds/core` module (`vi.mock('@xds/core/Text',
|
|
87
|
+
// factory)`). We only rewrite override keys inside such a factory, so scope the
|
|
88
|
+
// detection tightly: callee must be `vi.mock`/`vi.doMock`/`jest.mock`/
|
|
89
|
+
// `jest.doMock` (or a bare `mock`) AND the first argument must be a string
|
|
90
|
+
// literal that resolves to `@xds/core` (bare or subpath).
|
|
91
|
+
const MOCK_METHOD_NAMES = new Set(['mock', 'doMock']);
|
|
92
|
+
const MOCK_OBJECT_NAMES = new Set(['vi', 'jest']);
|
|
93
|
+
|
|
94
|
+
function isMockCallee(callee) {
|
|
95
|
+
if (
|
|
96
|
+
callee.type === 'MemberExpression' &&
|
|
97
|
+
!callee.computed &&
|
|
98
|
+
callee.object.type === 'Identifier' &&
|
|
99
|
+
MOCK_OBJECT_NAMES.has(callee.object.name) &&
|
|
100
|
+
callee.property.type === 'Identifier' &&
|
|
101
|
+
MOCK_METHOD_NAMES.has(callee.property.name)
|
|
102
|
+
) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return callee.type === 'Identifier' && callee.name === 'mock';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isXdsCoreMockCall(node) {
|
|
109
|
+
if (node.type !== 'CallExpression' || !isMockCallee(node.callee))
|
|
110
|
+
return false;
|
|
111
|
+
const [arg] = node.arguments;
|
|
112
|
+
const value =
|
|
113
|
+
arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')
|
|
114
|
+
? arg.value
|
|
115
|
+
: null;
|
|
116
|
+
return typeof value === 'string' && XDS_CORE_SOURCE.test(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Rename XDS-prefixed object-property KEYS (not values, not nested objects)
|
|
120
|
+
// within the given AST subtree. Used only on a recognized @xds/core mock
|
|
121
|
+
// factory, so the scope guard lives at the call site.
|
|
122
|
+
function renameMockFactoryKeys(node, seen, onChange) {
|
|
123
|
+
if (!node || typeof node !== 'object') return;
|
|
124
|
+
if (Array.isArray(node)) {
|
|
125
|
+
for (const child of node) renameMockFactoryKeys(child, seen, onChange);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (seen.has(node)) return;
|
|
129
|
+
seen.add(node);
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
(node.type === 'ObjectProperty' || node.type === 'Property') &&
|
|
133
|
+
!node.computed &&
|
|
134
|
+
node.key &&
|
|
135
|
+
node.key.type === 'Identifier'
|
|
136
|
+
) {
|
|
137
|
+
const bare = bareName(node.key.name);
|
|
138
|
+
if (bare && bare !== node.key.name) {
|
|
139
|
+
node.key.name = bare;
|
|
140
|
+
onChange();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const key of Object.keys(node)) {
|
|
145
|
+
if (
|
|
146
|
+
key === 'loc' ||
|
|
147
|
+
key === 'start' ||
|
|
148
|
+
key === 'end' ||
|
|
149
|
+
key === 'range' ||
|
|
150
|
+
key === 'comments' ||
|
|
151
|
+
key === 'leadingComments' ||
|
|
152
|
+
key === 'trailingComments' ||
|
|
153
|
+
key === 'tokens'
|
|
154
|
+
) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
renameMockFactoryKeys(node[key], seen, onChange);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
77
161
|
export default function transformer(file, api) {
|
|
78
162
|
const j = api.jscodeshift;
|
|
79
163
|
const root = j(file.source);
|
|
@@ -190,11 +274,26 @@ export default function transformer(file, api) {
|
|
|
190
274
|
}
|
|
191
275
|
});
|
|
192
276
|
|
|
277
|
+
// 3. Un-prefix override keys inside @xds/core mock factories. Independent of
|
|
278
|
+
// the import bindings above: the override key is a factory object property,
|
|
279
|
+
// not a reference to an imported binding, so it must match the renamed export
|
|
280
|
+
// name of the mocked module regardless of what the file imports.
|
|
281
|
+
const seenMockNodes = new Set();
|
|
282
|
+
root.find(j.CallExpression).forEach(path => {
|
|
283
|
+
if (!isXdsCoreMockCall(path.node)) return;
|
|
284
|
+
// Only walk the factory argument(s), never the module-path string.
|
|
285
|
+
for (const arg of path.node.arguments.slice(1)) {
|
|
286
|
+
renameMockFactoryKeys(arg, seenMockNodes, () => {
|
|
287
|
+
hasChanges = true;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
193
292
|
if (localRenames.size === 0) {
|
|
194
293
|
return hasChanges ? root.toSource() : undefined;
|
|
195
294
|
}
|
|
196
295
|
|
|
197
|
-
//
|
|
296
|
+
// 4. Rename references to renamed local bindings
|
|
198
297
|
// Identifiers (type refs, value refs, etc.)
|
|
199
298
|
root.find(j.Identifier).forEach(path => {
|
|
200
299
|
const newName = localRenames.get(path.node.name);
|
|
@@ -231,7 +330,7 @@ export default function transformer(file, api) {
|
|
|
231
330
|
}
|
|
232
331
|
});
|
|
233
332
|
|
|
234
|
-
//
|
|
333
|
+
// 5. Rename TS type references in generic type-argument positions.
|
|
235
334
|
//
|
|
236
335
|
// `j.find(j.Identifier)` (and `j.find(j.TSTypeReference)`) do NOT visit
|
|
237
336
|
// identifiers nested inside generic type arguments with the tsx/babel parser
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
export const meta = {
|
|
13
13
|
title: 'Migrate module specifiers from @xds/* to @astryxdesign/*',
|
|
14
14
|
description:
|
|
15
|
-
'Updates JS/TS import, export, dynamic import,
|
|
16
|
-
'
|
|
15
|
+
'Updates JS/TS import, export, dynamic import, require, TS import-type, ' +
|
|
16
|
+
'and test-framework mock (vi.mock / jest.mock) module specifiers from ' +
|
|
17
|
+
'@xds/* to the @astryxdesign/* packages used by Astryx v0.1.0.',
|
|
17
18
|
pr: '#3092',
|
|
18
19
|
fileExtensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
|
|
19
20
|
};
|
|
@@ -73,6 +74,30 @@ function isCollapsedThemeSource(value) {
|
|
|
73
74
|
return false;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// Test-framework mock calls take the mocked module path as their first
|
|
78
|
+
// argument (`vi.mock('@xds/core/Text', factory)`). That path is a plain string
|
|
79
|
+
// literal -- not an ImportDeclaration source -- so it slips past the import/
|
|
80
|
+
// export handlers and, left unrewritten, the mock would target the stale
|
|
81
|
+
// `@xds/*` path and stop intercepting the renamed `@astryxdesign/*` import.
|
|
82
|
+
// Recognized callees: `vi.mock`, `vi.doMock`, `jest.mock`, `jest.doMock`, and
|
|
83
|
+
// a bare `mock(...)` (destructured from the test runner).
|
|
84
|
+
const MOCK_METHOD_NAMES = new Set(['mock', 'doMock']);
|
|
85
|
+
const MOCK_OBJECT_NAMES = new Set(['vi', 'jest']);
|
|
86
|
+
|
|
87
|
+
function isMockCall(callee) {
|
|
88
|
+
if (
|
|
89
|
+
callee.type === 'MemberExpression' &&
|
|
90
|
+
!callee.computed &&
|
|
91
|
+
callee.object.type === 'Identifier' &&
|
|
92
|
+
MOCK_OBJECT_NAMES.has(callee.object.name) &&
|
|
93
|
+
callee.property.type === 'Identifier' &&
|
|
94
|
+
MOCK_METHOD_NAMES.has(callee.property.name)
|
|
95
|
+
) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return callee.type === 'Identifier' && callee.name === 'mock';
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
function remapThemeExportNames(importPath, j) {
|
|
77
102
|
let changed = false;
|
|
78
103
|
importPath.node.specifiers = (importPath.node.specifiers || []).map(spec => {
|
|
@@ -118,12 +143,53 @@ export default function transformer(file, api) {
|
|
|
118
143
|
const isRequire =
|
|
119
144
|
path.node.callee.type === 'Identifier' &&
|
|
120
145
|
path.node.callee.name === 'require';
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
if (isDynamicImport || isRequire || isMockCall(path.node.callee)) {
|
|
147
|
+
const [arg] = path.node.arguments;
|
|
148
|
+
if (arg?.type === 'StringLiteral' || arg?.type === 'Literal') {
|
|
149
|
+
hasChanges = rewriteLiteral(arg) || hasChanges;
|
|
150
|
+
}
|
|
125
151
|
}
|
|
126
152
|
});
|
|
127
153
|
|
|
154
|
+
// TS `import(...)` TYPE specifiers -- e.g. the `import('@xds/core/Text')` in
|
|
155
|
+
// `typeof import('@xds/core/Text')` -- parse to a `TSImportType` node, not a
|
|
156
|
+
// dynamic-import CallExpression, so the handler above never reaches them.
|
|
157
|
+
// ast-types' traversal also doesn't descend into a `TSImportType` nested in a
|
|
158
|
+
// generic type-argument position (e.g. `orig<typeof import('@xds/core/Text')>()`),
|
|
159
|
+
// so `j.find(j.TSImportType)` misses it. Walk the raw AST to cover every
|
|
160
|
+
// position. Re-visiting a node the loop above handled is harmless: the
|
|
161
|
+
// literal is already rewritten, so the second pass is a no-op.
|
|
162
|
+
const seenTypeImportNodes = new Set();
|
|
163
|
+
const rewriteTSImportTypes = node => {
|
|
164
|
+
if (!node || typeof node !== 'object') return;
|
|
165
|
+
if (Array.isArray(node)) {
|
|
166
|
+
for (const child of node) rewriteTSImportTypes(child);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (seenTypeImportNodes.has(node)) return;
|
|
170
|
+
seenTypeImportNodes.add(node);
|
|
171
|
+
|
|
172
|
+
if (node.type === 'TSImportType' && node.argument) {
|
|
173
|
+
hasChanges = rewriteLiteral(node.argument) || hasChanges;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const key of Object.keys(node)) {
|
|
177
|
+
if (
|
|
178
|
+
key === 'loc' ||
|
|
179
|
+
key === 'start' ||
|
|
180
|
+
key === 'end' ||
|
|
181
|
+
key === 'range' ||
|
|
182
|
+
key === 'comments' ||
|
|
183
|
+
key === 'leadingComments' ||
|
|
184
|
+
key === 'trailingComments' ||
|
|
185
|
+
key === 'tokens'
|
|
186
|
+
) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
rewriteTSImportTypes(node[key]);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
rewriteTSImportTypes(root.get().node);
|
|
193
|
+
|
|
128
194
|
return hasChanges ? root.toSource() : undefined;
|
|
129
195
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyTransform(source) {
|
|
6
|
+
const {default: transform} = await import('../rename-avatar-size-scale.mjs');
|
|
7
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
8
|
+
const j = jscodeshift.withParser('tsx');
|
|
9
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
10
|
+
const file = {source, path: 'test.tsx'};
|
|
11
|
+
const result = transform(file, api);
|
|
12
|
+
return result ?? source;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
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';
|
|
18
|
+
const a = <Avatar name="A" size="tiny" />;
|
|
19
|
+
const b = <Avatar name="B" size="xsmall" />;
|
|
20
|
+
const c = <Avatar name="C" size="small" />;
|
|
21
|
+
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}"`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
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
|
+
});
|
|
40
|
+
|
|
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}');
|
|
46
|
+
});
|
|
47
|
+
|
|
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
|
+
});
|
|
54
|
+
|
|
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'`);
|
|
62
|
+
});
|
|
63
|
+
|
|
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
|
+
});
|
|
70
|
+
|
|
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'`);
|
|
80
|
+
});
|
|
81
|
+
|
|
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
|
+
});
|
|
92
|
+
|
|
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']`);
|
|
98
|
+
});
|
|
99
|
+
|
|
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
|
+
});
|
|
108
|
+
|
|
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'`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- Precision guards: ambiguous common words must NOT be corrupted in
|
|
117
|
+
// --- context-blind positions, even in files that import Avatar. ---
|
|
118
|
+
|
|
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'`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does NOT rename an ambiguous name in a non-size object property', async () => {
|
|
128
|
+
const input = `import {Avatar} from '@astryxdesign/core';
|
|
129
|
+
const cfg = {priority: 'medium', density: 'large'};`;
|
|
130
|
+
const output = await applyTransform(input);
|
|
131
|
+
expect(output).toContain(`priority: 'medium'`);
|
|
132
|
+
expect(output).toContain(`density: 'large'`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('does NOT rename an ambiguous name in a size-keyed object property (component unknown)', async () => {
|
|
136
|
+
// A `size: 'small'` object entry could belong to any component; renaming it
|
|
137
|
+
// blind would be unsafe, so it is left for manual migration.
|
|
138
|
+
const input = `import {Avatar} from '@astryxdesign/core';
|
|
139
|
+
const args = {size: 'small'};`;
|
|
140
|
+
const output = await applyTransform(input);
|
|
141
|
+
expect(output).toContain(`size: 'small'`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not touch files that never import Avatar/AvatarGroup', async () => {
|
|
145
|
+
const input = `import {Badge} from '@astryxdesign/core';
|
|
146
|
+
const x = <Badge size="small" />;
|
|
147
|
+
type P = 'tiny' | 'xsmall';`;
|
|
148
|
+
const output = await applyTransform(input);
|
|
149
|
+
expect(output).toContain(`size="small"`);
|
|
150
|
+
expect(output).toContain(`'tiny' | 'xsmall'`);
|
|
151
|
+
});
|
|
152
|
+
|
|
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'`);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file v0.1.8 transform manifest
|
|
5
|
+
*
|
|
6
|
+
* Lists all codemods for the v0.1.8 release in the order they should run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import renameAvatarSizeScale, {
|
|
10
|
+
meta as renameAvatarSizeScaleMeta,
|
|
11
|
+
} from './rename-avatar-size-scale.mjs';
|
|
12
|
+
|
|
13
|
+
export default [
|
|
14
|
+
{
|
|
15
|
+
name: 'rename-avatar-size-scale',
|
|
16
|
+
transform: renameAvatarSizeScale,
|
|
17
|
+
meta: renameAvatarSizeScaleMeta,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Rename Avatar named sizes to Icon's abbreviated scale
|
|
5
|
+
* @see https://github.com/facebook/astryx/issues/2672
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const meta = {
|
|
35
|
+
title: 'Rename Avatar named sizes tiny/xsmall/small/medium/large → xsm/sm/md/lg/xl',
|
|
36
|
+
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.',
|
|
43
|
+
pr: '#2672',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Import sources that provide the Astryx Avatar/AvatarGroup components. */
|
|
47
|
+
const IMPORT_SOURCES = new Set([
|
|
48
|
+
'@astryxdesign/core',
|
|
49
|
+
'@astryxdesign/core/Avatar',
|
|
50
|
+
'@astryxdesign/core/AvatarGroup',
|
|
51
|
+
'@xds/core',
|
|
52
|
+
'@xds/core/Avatar',
|
|
53
|
+
'@xds/core/AvatarGroup',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** Component names whose `size` prop is renamed. */
|
|
57
|
+
const TARGET_IMPORTED_NAMES = new Set(['Avatar', 'AvatarGroup']);
|
|
58
|
+
|
|
59
|
+
/** Old → new size-name mapping. */
|
|
60
|
+
const RENAMES = new Map([
|
|
61
|
+
['tiny', 'xsm'],
|
|
62
|
+
['xsmall', 'sm'],
|
|
63
|
+
['small', 'md'],
|
|
64
|
+
['medium', 'lg'],
|
|
65
|
+
['large', 'xl'],
|
|
66
|
+
]);
|
|
67
|
+
|
|
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(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;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Extract a plain string value from a literal-ish node, or null. */
|
|
98
|
+
function literalString(node) {
|
|
99
|
+
const target = node?.type === 'TSAsExpression' ? node.expression : node;
|
|
100
|
+
if (
|
|
101
|
+
target &&
|
|
102
|
+
(target.type === 'StringLiteral' || target.type === 'Literal') &&
|
|
103
|
+
typeof target.value === 'string'
|
|
104
|
+
) {
|
|
105
|
+
return target.value;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default function transformer(file, api) {
|
|
111
|
+
const j = api.jscodeshift;
|
|
112
|
+
const root = j(file.source);
|
|
113
|
+
let hasChanges = false;
|
|
114
|
+
|
|
115
|
+
// Track alias-aware local names for the target components.
|
|
116
|
+
const targetLocals = new Set();
|
|
117
|
+
root.find(j.ImportDeclaration).forEach((path) => {
|
|
118
|
+
if (!IMPORT_SOURCES.has(path.node.source.value)) return;
|
|
119
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
120
|
+
if (
|
|
121
|
+
spec.type === 'ImportSpecifier' &&
|
|
122
|
+
TARGET_IMPORTED_NAMES.has(spec.imported.name)
|
|
123
|
+
) {
|
|
124
|
+
targetLocals.add(spec.local.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (targetLocals.size === 0) return undefined;
|
|
130
|
+
|
|
131
|
+
function renameArrayElements(node) {
|
|
132
|
+
let arr = node;
|
|
133
|
+
if (arr.type === 'TSAsExpression') arr = arr.expression;
|
|
134
|
+
if (arr.type !== 'ArrayExpression') return;
|
|
135
|
+
// If the array contains a name unique to Avatar's scale (tiny/xsmall), the
|
|
136
|
+
// whole collection is unambiguously the Avatar size enum, so it is safe to
|
|
137
|
+
// rename the ambiguous members too. Otherwise stay conservative.
|
|
138
|
+
const precise = arr.elements.some((el) => UNIQUE.has(literalString(el)));
|
|
139
|
+
for (const el of arr.elements) {
|
|
140
|
+
if (renameValue(el, {precise})) hasChanges = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 1. `size` JSX attributes on target components — PRECISE (component known),
|
|
145
|
+
// so all five names, including the ambiguous ones, are renamed here.
|
|
146
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
147
|
+
const name = path.node.name;
|
|
148
|
+
const componentName = name.type === 'JSXIdentifier' ? name.name : null;
|
|
149
|
+
if (!componentName || !targetLocals.has(componentName)) return;
|
|
150
|
+
|
|
151
|
+
for (const attr of path.node.attributes) {
|
|
152
|
+
if (attr.type !== 'JSXAttribute' || attr.name?.name !== 'size') continue;
|
|
153
|
+
const value = attr.value;
|
|
154
|
+
|
|
155
|
+
// size="small"
|
|
156
|
+
if (renameValue(value, {precise: true})) {
|
|
157
|
+
hasChanges = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
value &&
|
|
162
|
+
value.type === 'JSXExpressionContainer' &&
|
|
163
|
+
value.expression
|
|
164
|
+
) {
|
|
165
|
+
// size={'small'}
|
|
166
|
+
if (renameValue(value.expression, {precise: true})) {
|
|
167
|
+
hasChanges = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// size={cond ? 'small' : 'large'}
|
|
171
|
+
if (value.expression.type === 'ConditionalExpression') {
|
|
172
|
+
if (renameValue(value.expression.consequent, {precise: true}))
|
|
173
|
+
hasChanges = true;
|
|
174
|
+
if (renameValue(value.expression.alternate, {precise: true}))
|
|
175
|
+
hasChanges = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 2. Object properties keyed `size` (or `*Size`) + Storybook `options`
|
|
182
|
+
// arrays — NON-PRECISE (the component isn't known here), so only the
|
|
183
|
+
// unique names tiny/xsmall are renamed.
|
|
184
|
+
const PropertyType = j.ObjectProperty ?? j.Property;
|
|
185
|
+
root.find(PropertyType).forEach((path) => {
|
|
186
|
+
const key = path.node.key;
|
|
187
|
+
const keyName =
|
|
188
|
+
key.type === 'Identifier'
|
|
189
|
+
? key.name
|
|
190
|
+
: key.type === 'StringLiteral' || key.type === 'Literal'
|
|
191
|
+
? key.value
|
|
192
|
+
: null;
|
|
193
|
+
if (typeof keyName !== 'string') return;
|
|
194
|
+
|
|
195
|
+
const lower = keyName.toLowerCase();
|
|
196
|
+
if (lower !== 'size' && !lower.endsWith('size')) return;
|
|
197
|
+
|
|
198
|
+
const value = path.node.value;
|
|
199
|
+
// size: 'tiny'
|
|
200
|
+
if (renameValue(value)) {
|
|
201
|
+
hasChanges = true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// size: { control: 'select', options: ['tiny', ...] }
|
|
205
|
+
if (value.type === 'ObjectExpression') {
|
|
206
|
+
const optionsProp = value.properties.find(
|
|
207
|
+
(p) => p.key && (p.key.name === 'options' || p.key.value === 'options'),
|
|
208
|
+
);
|
|
209
|
+
if (optionsProp) renameArrayElements(optionsProp.value);
|
|
210
|
+
}
|
|
211
|
+
// size: ['tiny', 'xsmall', ...]
|
|
212
|
+
renameArrayElements(value);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 3. Union-type literals — NON-PRECISE, so only the unique names tiny/xsmall
|
|
216
|
+
// are renamed. This avoids corrupting unrelated unions that happen to use
|
|
217
|
+
// common words like 'medium' (e.g. a priority or breakpoint type) in a
|
|
218
|
+
// file that also imports Avatar.
|
|
219
|
+
root.find(j.TSLiteralType).forEach((path) => {
|
|
220
|
+
if (renameValue(path.node.literal)) hasChanges = true;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 4. Standalone array literals whose members are exactly the Avatar size
|
|
224
|
+
// scale (identified by the presence of a unique name like tiny/xsmall) —
|
|
225
|
+
// e.g. `(['tiny', 'xsmall', 'small', 'medium', 'large'] as const).map(...)`
|
|
226
|
+
// used to render every size. The unique name proves the whole array is the
|
|
227
|
+
// Avatar enum, so its ambiguous members can be renamed safely.
|
|
228
|
+
root.find(j.ArrayExpression).forEach((path) => {
|
|
229
|
+
renameArrayElements(path.node);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!hasChanges) return undefined;
|
|
233
|
+
return root.toSource({quote: 'single'});
|
|
234
|
+
}
|