@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.dea6813
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -57
- package/api/component/component.doc.mjs +1 -1
- package/api/component/component.mjs +3 -3
- package/api/component/component.type.d.mts +4 -5
- package/api/component/component.type.mjs +4 -5
- package/api/component/detail/blocks/blocks.d.mts +1 -2
- package/api/component/detail/blocks/blocks.mjs +3 -4
- package/api/component/list/list.d.mts +5 -0
- package/api/component/list/list.mjs +9 -37
- package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +165 -86
- package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +210 -72
- package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +115 -83
- package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +186 -57
- package/clients/cli/commands/component-package.test.mjs +0 -28
- package/foundation/response/response-types.doc.mjs +7 -7
- package/package.json +9 -9
- package/assets/codemods/transform-prop.mjs +0 -109
- package/assets/codemods/transform-prop.test.mjs +0 -95
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +0 -61
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +0 -126
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +0 -15
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +0 -86
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +0 -99
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +0 -122
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +0 -14
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +0 -95
- package/foundation/response/response-types.doc.test.mjs +0 -158
|
@@ -1,129 +1,161 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
import {describe,
|
|
4
|
-
import transform from '../rename-avatar-size-scale.mjs';
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
5
4
|
|
|
6
5
|
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
|
-
|
|
10
|
+
const file = {source, path: 'test.tsx'};
|
|
11
|
+
const result = transform(file, api);
|
|
12
|
+
return result ?? source;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
describe('rename-avatar-size-scale', () => {
|
|
14
|
-
it('renames every
|
|
15
|
-
const
|
|
16
|
-
await applyTransform(`import {Avatar} from '@astryxdesign/core';
|
|
16
|
+
it('renames every named size on an Avatar JSX size attribute', async () => {
|
|
17
|
+
const input = `import {Avatar} from '@astryxdesign/core';
|
|
17
18
|
const a = <Avatar name="A" size="tiny" />;
|
|
18
19
|
const b = <Avatar name="B" size="xsmall" />;
|
|
19
20
|
const c = <Avatar name="C" size="small" />;
|
|
20
21
|
const d = <Avatar name="D" size="medium" />;
|
|
21
|
-
const e = <Avatar name="E" size="large"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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}"`);
|
|
25
31
|
}
|
|
26
32
|
});
|
|
27
33
|
|
|
28
|
-
it('renames
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
expect(output).toContain("{'md' satisfies AvatarSize}");
|
|
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'`);
|
|
34
39
|
});
|
|
35
40
|
|
|
36
|
-
it('
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
expect(output).toContain("large ? 'xl' : 'md'");
|
|
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}');
|
|
42
46
|
});
|
|
43
47
|
|
|
44
|
-
it('
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expect(output).toContain("<Face size='lg'");
|
|
51
|
-
expect(output).toContain("<Faces size='xl'");
|
|
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'`);
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
it('
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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'`);
|
|
60
62
|
});
|
|
61
63
|
|
|
62
|
-
it('
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
+
});
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
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'`);
|
|
70
80
|
});
|
|
71
81
|
|
|
72
|
-
it('
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
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
|
+
});
|
|
76
92
|
|
|
77
|
-
|
|
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']`);
|
|
78
98
|
});
|
|
79
99
|
|
|
80
|
-
it('
|
|
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.
|
|
81
103
|
const input = `import {Avatar} from '@astryxdesign/core';
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
expect(await applyTransform(input)).toBe(input);
|
|
104
|
+
const densities = ['small', 'medium', 'large'] as const;`;
|
|
105
|
+
const output = await applyTransform(input);
|
|
106
|
+
expect(output).toContain(`['small', 'medium', 'large']`);
|
|
86
107
|
});
|
|
87
108
|
|
|
88
|
-
it('
|
|
109
|
+
it('renames a UNIQUE name in a size-typed union literal', async () => {
|
|
89
110
|
const input = `import {Avatar} from '@astryxdesign/core';
|
|
90
|
-
type
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const meta = {argTypes: {size: {options: sizes}}};
|
|
94
|
-
const view = <Avatar size={card.size} />;`;
|
|
95
|
-
|
|
96
|
-
expect(await applyTransform(input)).toBe(input);
|
|
111
|
+
type Props = {size: 'tiny' | 'xsmall'};`;
|
|
112
|
+
const output = await applyTransform(input);
|
|
113
|
+
expect(output).toContain(`'xsm' | 'sm'`);
|
|
97
114
|
});
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const view = <Badge size="small" />;`;
|
|
102
|
-
expect(await applyTransform(input)).toBe(input);
|
|
103
|
-
});
|
|
116
|
+
// --- Precision guards: ambiguous common words must NOT be corrupted in
|
|
117
|
+
// --- context-blind positions, even in files that import Avatar. ---
|
|
104
118
|
|
|
105
|
-
it('
|
|
106
|
-
const input = `import {Avatar} from '@
|
|
107
|
-
|
|
108
|
-
|
|
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'`);
|
|
109
125
|
});
|
|
110
126
|
|
|
111
|
-
it('
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
+
});
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
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'`);
|
|
119
142
|
});
|
|
120
143
|
|
|
121
|
-
it('
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
});
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
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'`);
|
|
128
160
|
});
|
|
129
161
|
});
|
|
@@ -4,23 +4,46 @@
|
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
32
|
*/
|
|
13
33
|
|
|
14
|
-
import {transformProp} from '../../transform-prop.mjs';
|
|
15
|
-
|
|
16
34
|
export const meta = {
|
|
17
35
|
title: 'Rename Avatar named sizes tiny/xsmall/small/medium/large → xsm/sm/md/lg/xl',
|
|
18
36
|
description:
|
|
19
|
-
'
|
|
20
|
-
'
|
|
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.',
|
|
21
43
|
pr: '#2672',
|
|
22
44
|
};
|
|
23
45
|
|
|
46
|
+
/** Import sources that provide the Astryx Avatar/AvatarGroup components. */
|
|
24
47
|
const IMPORT_SOURCES = new Set([
|
|
25
48
|
'@astryxdesign/core',
|
|
26
49
|
'@astryxdesign/core/Avatar',
|
|
@@ -30,11 +53,10 @@ const IMPORT_SOURCES = new Set([
|
|
|
30
53
|
'@xds/core/AvatarGroup',
|
|
31
54
|
]);
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
['AvatarGroup', new Set(['size'])],
|
|
36
|
-
]);
|
|
56
|
+
/** Component names whose `size` prop is renamed. */
|
|
57
|
+
const TARGET_IMPORTED_NAMES = new Set(['Avatar', 'AvatarGroup']);
|
|
37
58
|
|
|
59
|
+
/** Old → new size-name mapping. */
|
|
38
60
|
const RENAMES = new Map([
|
|
39
61
|
['tiny', 'xsm'],
|
|
40
62
|
['xsmall', 'sm'],
|
|
@@ -43,38 +65,46 @@ const RENAMES = new Map([
|
|
|
43
65
|
['large', 'xl'],
|
|
44
66
|
]);
|
|
45
67
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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']);
|
|
50
77
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
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} = {}) {
|
|
53
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(/** @type {any} */ node) {
|
|
99
|
+
const target = node?.type === 'TSAsExpression' ? node.expression : node;
|
|
54
100
|
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
node.type === 'ParenthesizedExpression'
|
|
59
|
-
) {
|
|
60
|
-
return renameStaticValue(node.expression);
|
|
61
|
-
}
|
|
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'
|
|
101
|
+
target &&
|
|
102
|
+
(target.type === 'StringLiteral' || target.type === 'Literal') &&
|
|
103
|
+
typeof target.value === 'string'
|
|
70
104
|
) {
|
|
71
|
-
return
|
|
105
|
+
return target.value;
|
|
72
106
|
}
|
|
73
|
-
|
|
74
|
-
if (!replacement) return false;
|
|
75
|
-
node.value = replacement;
|
|
76
|
-
if (node.raw) node.raw = undefined;
|
|
77
|
-
return true;
|
|
107
|
+
return null;
|
|
78
108
|
}
|
|
79
109
|
|
|
80
110
|
/**
|
|
@@ -87,23 +117,122 @@ export default function transformer(file, api) {
|
|
|
87
117
|
const root = j(file.source);
|
|
88
118
|
let hasChanges = false;
|
|
89
119
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
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
|
+
});
|
|
107
236
|
|
|
108
237
|
if (!hasChanges) return undefined;
|
|
109
238
|
return root.toSource({quote: 'single'});
|
|
@@ -5,7 +5,6 @@ import * as fs from 'node:fs';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import {component} from '../../../api/component/component.mjs';
|
|
8
|
-
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
9
8
|
|
|
10
9
|
// These tests create a minimal monorepo fixture with:
|
|
11
10
|
// - packages/core (symlinked to real @astryxdesign/core for loadDocs compatibility)
|
|
@@ -139,31 +138,4 @@ describe('component() with --package option', () => {
|
|
|
139
138
|
expect(scoped.type).toBe('component.detail.blocks');
|
|
140
139
|
expect(scoped.data).toEqual(unscoped.data);
|
|
141
140
|
});
|
|
142
|
-
|
|
143
|
-
// `cwd` is an explicit API input: --blocks must discover from it, as
|
|
144
|
-
// --showcase does, not from the process working directory.
|
|
145
|
-
it('--blocks discovers blocks from options.cwd, like --showcase', async () => {
|
|
146
|
-
const showcase = await component('ProfileCard', {cwd: tmpDir, showcase: true});
|
|
147
|
-
expect(showcase.data.source).toContain('ProfileCardShowcase');
|
|
148
|
-
|
|
149
|
-
const blocks = await component('ProfileCard', {cwd: tmpDir, blocks: true});
|
|
150
|
-
expect(blocks.type).toBe('component.detail.blocks');
|
|
151
|
-
expect(blocks.data.showcase?.name).toBe('ProfileCardShowcase');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// List and detail report one import for a legacy package component; the
|
|
155
|
-
// text list projects the JSON value instead of deriving a Core path.
|
|
156
|
-
it('the names list gives a legacy package component the import its detail reports', async () => {
|
|
157
|
-
const detail = await component('ProfileCard', {cwd: tmpDir});
|
|
158
|
-
const list = await component(undefined, {cwd: tmpDir, list: true});
|
|
159
|
-
const entry = Object.values(list.data.components)
|
|
160
|
-
.flat()
|
|
161
|
-
.find(e => e.name === 'ProfileCard' && e.package === '@test/ext');
|
|
162
|
-
expect(entry?.import).toBe(detail.data.import);
|
|
163
|
-
|
|
164
|
-
const text = await runCli(['component', '--list'], tmpDir);
|
|
165
|
-
expect(text.code).toBe(0);
|
|
166
|
-
expect(text.stdout).toContain(`name: ProfileCard\nimport: ${detail.data.import} [@test/ext]`);
|
|
167
|
-
expect(text.stdout).not.toMatch(/^import: +@astryxdesign\/core\S* +\[@test\/ext\]$/m);
|
|
168
|
-
});
|
|
169
141
|
});
|
|
@@ -36,7 +36,7 @@ export const doc = {
|
|
|
36
36
|
{
|
|
37
37
|
value: 'component.detail',
|
|
38
38
|
description:
|
|
39
|
-
"One component's authored ComponentDoc plus ownership
|
|
39
|
+
"One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available).",
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
value: 'component.detail.props',
|
|
@@ -71,7 +71,7 @@ export const doc = {
|
|
|
71
71
|
{
|
|
72
72
|
value: 'docs.index',
|
|
73
73
|
description:
|
|
74
|
-
"One topic's section index (--index):
|
|
74
|
+
"One topic's section index (--index): each section's key, title, and one-line summary.",
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
value: 'docs.detail.section',
|
|
@@ -116,7 +116,7 @@ export const doc = {
|
|
|
116
116
|
{
|
|
117
117
|
value: 'search',
|
|
118
118
|
description:
|
|
119
|
-
'The echoed query, `matchCount` (total
|
|
119
|
+
'The echoed query, `matchCount` (how many candidates matched in total, before `limit`), plus a ranked SearchResultEntry[] bounded by `limit` (domain, name, score, reason, description, follow-up command, and import path where relevant).',
|
|
120
120
|
},
|
|
121
121
|
|
|
122
122
|
// build
|
|
@@ -128,7 +128,7 @@ export const doc = {
|
|
|
128
128
|
{
|
|
129
129
|
value: 'build.kit',
|
|
130
130
|
description:
|
|
131
|
-
'The composition kit: echoed query, hasResults
|
|
131
|
+
'The grouped composition kit: echoed query, hasResults/matchCount/directMatch fields (matchCount is the total matched, never a cap read back), the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays.',
|
|
132
132
|
},
|
|
133
133
|
|
|
134
134
|
// swizzle
|
|
@@ -151,7 +151,7 @@ export const doc = {
|
|
|
151
151
|
{
|
|
152
152
|
value: 'gap-report.file',
|
|
153
153
|
description:
|
|
154
|
-
'An aggregate receipt
|
|
154
|
+
'An aggregate receipt with overall status, the selected package and issues URL, ordered per-handler deliveries, and filedCount/routedOnlyCount totals.',
|
|
155
155
|
},
|
|
156
156
|
|
|
157
157
|
// template
|
|
@@ -228,7 +228,7 @@ export const doc = {
|
|
|
228
228
|
{
|
|
229
229
|
value: 'theme.targets',
|
|
230
230
|
description:
|
|
231
|
-
'The whole themeable surface: the echoed filter,
|
|
231
|
+
'The whole themeable surface: the echoed filter, the component count, and one entry per theming target — {key, className, component, props, states}, where props and states are its legal override keys.',
|
|
232
232
|
},
|
|
233
233
|
{
|
|
234
234
|
value: 'theme.palette.generate',
|
|
@@ -276,7 +276,7 @@ export const doc = {
|
|
|
276
276
|
{
|
|
277
277
|
value: 'integration.pack-check',
|
|
278
278
|
description:
|
|
279
|
-
'The packed-package check:
|
|
279
|
+
'The packed-package check: package identity, tarball facts, local and packed contribution inventories, and issues.',
|
|
280
280
|
},
|
|
281
281
|
{
|
|
282
282
|
value: 'integration.validate',
|