@astryxdesign/cli 0.4.7-canary.581f844 → 0.4.7-canary.5da424d
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/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +127 -0
- package/assets/codemods/transforms/next/banner-collapsible-content.mjs +171 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
- package/package.json +9 -9
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Unit tests for the staged (next-release) codemods.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors v0.4.0/__tests__/next-codemods.test.mjs, which covers the codemods
|
|
7
|
+
* after promotion. Keeping a copy here means a staged transform is tested from
|
|
8
|
+
* the day it is written rather than the day it is released.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, expect, it} from 'vitest';
|
|
12
|
+
import jscodeshift from 'jscodeshift';
|
|
13
|
+
|
|
14
|
+
const j = jscodeshift.withParser('tsx');
|
|
15
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
16
|
+
|
|
17
|
+
async function apply(name, source) {
|
|
18
|
+
const {default: transform} = await import(`../${name}.mjs`);
|
|
19
|
+
return transform({source, path: 'test.tsx'}, api) ?? source;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TRANSFORM = 'banner-collapsible-content';
|
|
23
|
+
|
|
24
|
+
const IMPORT = "import {Banner} from '@astryxdesign/core/Banner';\n";
|
|
25
|
+
|
|
26
|
+
describe('banner-collapsible-content', () => {
|
|
27
|
+
it('rewrites a bare defaultIsExpanded to a starts-open config', async () => {
|
|
28
|
+
const output = await apply(
|
|
29
|
+
TRANSFORM,
|
|
30
|
+
`${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded><p>d</p></Banner>;`,
|
|
31
|
+
);
|
|
32
|
+
expect(output).toContain('defaultIsOpen: true');
|
|
33
|
+
expect(output).not.toContain('defaultIsExpanded');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rewrites defaultIsExpanded={true} the same way', async () => {
|
|
37
|
+
const output = await apply(
|
|
38
|
+
TRANSFORM,
|
|
39
|
+
`${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded={true}><p>d</p></Banner>;`,
|
|
40
|
+
);
|
|
41
|
+
expect(output).toContain('defaultIsOpen: true');
|
|
42
|
+
expect(output).not.toContain('defaultIsExpanded');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('drops defaultIsExpanded={false}, which is the default', async () => {
|
|
46
|
+
const output = await apply(
|
|
47
|
+
TRANSFORM,
|
|
48
|
+
`${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded={false}><p>d</p></Banner>;`,
|
|
49
|
+
);
|
|
50
|
+
expect(output).not.toContain('defaultIsExpanded');
|
|
51
|
+
// No config needed: starting collapsed is what a Banner does by default.
|
|
52
|
+
expect(output).not.toContain('collapsible');
|
|
53
|
+
// Untouched attributes keep their original text (recast only reprints
|
|
54
|
+
// what changed), so the element is exactly the base minus the prop.
|
|
55
|
+
expect(output).toContain('<Banner status="info" title="T">');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('keeps a dynamic default dynamic', async () => {
|
|
59
|
+
const output = await apply(
|
|
60
|
+
TRANSFORM,
|
|
61
|
+
`${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded={isOpen}><p>d</p></Banner>;`,
|
|
62
|
+
);
|
|
63
|
+
expect(output).toContain('defaultIsOpen: isOpen');
|
|
64
|
+
expect(output).not.toContain('defaultIsExpanded');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('leaves a banner that never set the prop alone', async () => {
|
|
68
|
+
// The default is unchanged, so this banner still behaves as it did. The
|
|
69
|
+
// migration must not touch it — that is the whole point of the shape.
|
|
70
|
+
const source = `${IMPORT}const el = <Banner status="error" title="T"><ul><li>a</li></ul></Banner>;`;
|
|
71
|
+
const output = await apply(TRANSFORM, source);
|
|
72
|
+
expect(output).toBe(source);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('leaves a childless banner alone', async () => {
|
|
76
|
+
const source = `${IMPORT}const el = <Banner status="info" title="T" />;`;
|
|
77
|
+
const output = await apply(TRANSFORM, source);
|
|
78
|
+
expect(output).toBe(source);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('leaves a banner that already uses collapsible alone', async () => {
|
|
82
|
+
const source = `${IMPORT}const el = <Banner status="info" title="T" collapsible={false} defaultIsExpanded><p>d</p></Banner>;`;
|
|
83
|
+
const output = await apply(TRANSFORM, source);
|
|
84
|
+
expect(output).toBe(source);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not guess around a spread', async () => {
|
|
88
|
+
const source = `${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded {...rest}><p>d</p></Banner>;`;
|
|
89
|
+
const output = await apply(TRANSFORM, source);
|
|
90
|
+
expect(output).toBe(source);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("leaves another component's defaultIsExpanded alone", async () => {
|
|
94
|
+
// ChatToolCalls has a prop of the same name that this migration must not
|
|
95
|
+
// touch.
|
|
96
|
+
const source = `import {ChatToolCalls} from '@astryxdesign/core/Chat';
|
|
97
|
+
const el = <ChatToolCalls calls={calls} defaultIsExpanded />;`;
|
|
98
|
+
const output = await apply(TRANSFORM, source);
|
|
99
|
+
expect(output).toBe(source);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('leaves a Banner that is not the core Banner alone', async () => {
|
|
103
|
+
const source = `import {Banner} from './ui/Banner';
|
|
104
|
+
const el = <Banner status="info" title="T" defaultIsExpanded><p>d</p></Banner>;`;
|
|
105
|
+
const output = await apply(TRANSFORM, source);
|
|
106
|
+
expect(output).toBe(source);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('leaves the prop inside a props object alone', async () => {
|
|
110
|
+
// Out of scope by design: which component the object feeds is a guess,
|
|
111
|
+
// and the removed prop makes those sites a type error anyway.
|
|
112
|
+
const source = `${IMPORT}const args = {status: 'info', title: 'T', defaultIsExpanded: true};
|
|
113
|
+
const el = <Banner {...args} />;`;
|
|
114
|
+
const output = await apply(TRANSFORM, source);
|
|
115
|
+
expect(output).toBe(source);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('migrates a Banner imported from the package root', async () => {
|
|
119
|
+
const output = await apply(
|
|
120
|
+
TRANSFORM,
|
|
121
|
+
`import {Banner, Button} from '@astryxdesign/core';
|
|
122
|
+
const el = <Banner status="info" title="T" defaultIsExpanded><p>d</p></Banner>;`,
|
|
123
|
+
);
|
|
124
|
+
expect(output).toContain('defaultIsOpen: true');
|
|
125
|
+
expect(output).not.toContain('defaultIsExpanded');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: migrate Banner's `defaultIsExpanded` onto `collapsible`
|
|
5
|
+
*
|
|
6
|
+
* Banner's collapse axis used to be a single knob, `defaultIsExpanded`, with
|
|
7
|
+
* the disclosure itself inferred from the presence of `children`. There was no
|
|
8
|
+
* way to have content without a toggle, and no controlled mode. The axis now
|
|
9
|
+
* lives on one `boolean | CollapsibleConfig` prop, per the boolean-or-config
|
|
10
|
+
* convention:
|
|
11
|
+
*
|
|
12
|
+
* <Banner>{children}</Banner> → collapsible, starts closed
|
|
13
|
+
* <Banner collapsible={{defaultIsOpen: true}}> → collapsible, starts open
|
|
14
|
+
* <Banner collapsible={{isOpen, onOpenChange}}> → controlled
|
|
15
|
+
* <Banner collapsible={false}> → always visible, no toggle
|
|
16
|
+
*
|
|
17
|
+
* The default is unchanged, so a Banner that never mentioned the old prop needs
|
|
18
|
+
* no rewrite at all — this transform is a prop rename and nothing more:
|
|
19
|
+
*
|
|
20
|
+
* defaultIsExpanded → collapsible={{defaultIsOpen: true}}
|
|
21
|
+
* defaultIsExpanded={true} → collapsible={{defaultIsOpen: true}}
|
|
22
|
+
* defaultIsExpanded={false} → (removed — it is the default)
|
|
23
|
+
* defaultIsExpanded={expr} → collapsible={{defaultIsOpen: expr}}
|
|
24
|
+
*
|
|
25
|
+
* Only elements named `Banner` are touched, and only when the file imports that
|
|
26
|
+
* name from `@astryxdesign/core`: `defaultIsExpanded` is also a ChatToolCalls
|
|
27
|
+
* prop, which this migration must leave alone.
|
|
28
|
+
*
|
|
29
|
+
* Scope: JSX attributes only. `defaultIsExpanded` inside a props object (a
|
|
30
|
+
* Storybook `args`, a spread built up in a variable) is left alone rather than
|
|
31
|
+
* rewritten on a guess about which component the object is for — removing the
|
|
32
|
+
* prop from the type makes those sites a type error, which is loud enough to
|
|
33
|
+
* find them.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const meta = {
|
|
37
|
+
title: "Rename Banner's `defaultIsExpanded` to the `collapsible` config",
|
|
38
|
+
description:
|
|
39
|
+
"Banner's collapse axis is now a single `collapsible?: boolean | " +
|
|
40
|
+
'CollapsibleConfig` prop. Rewrites `defaultIsExpanded` to the equivalent ' +
|
|
41
|
+
'config (`{defaultIsOpen: true}`), and drops ' +
|
|
42
|
+
'`defaultIsExpanded={false}`, which is the default. Banners that never ' +
|
|
43
|
+
'set the prop are untouched — the default is unchanged. Pass ' +
|
|
44
|
+
'`collapsible={false}` for content that is always visible.',
|
|
45
|
+
pr: '#5255',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const OLD_PROP = 'defaultIsExpanded';
|
|
49
|
+
const NEW_PROP = 'collapsible';
|
|
50
|
+
const COMPONENT = 'Banner';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Does this file use the core `Banner`?
|
|
54
|
+
*
|
|
55
|
+
* `defaultIsExpanded` is a ChatToolCalls prop too, and a local component may
|
|
56
|
+
* well be called Banner, so an unqualified element-name match is not enough.
|
|
57
|
+
*
|
|
58
|
+
* @param {any} j
|
|
59
|
+
* @param {any} root
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
function importsCoreBanner(j, root) {
|
|
63
|
+
let found = false;
|
|
64
|
+
root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
|
|
65
|
+
const source = path.node.source?.value;
|
|
66
|
+
if (typeof source !== 'string' || !source.startsWith('@astryxdesign/core')) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
70
|
+
if (
|
|
71
|
+
(spec.type === 'ImportSpecifier' && spec.imported?.name === COMPONENT) ||
|
|
72
|
+
(spec.local?.name === COMPONENT &&
|
|
73
|
+
(spec.type === 'ImportDefaultSpecifier' ||
|
|
74
|
+
spec.type === 'ImportSpecifier'))
|
|
75
|
+
) {
|
|
76
|
+
found = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
|
|
85
|
+
* @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
|
|
86
|
+
* @returns {string | null | undefined}
|
|
87
|
+
*/
|
|
88
|
+
export default function transformer(file, api) {
|
|
89
|
+
// Cheap bail-out: nothing to rename without the old prop.
|
|
90
|
+
if (!file.source.includes(OLD_PROP)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const j = api.jscodeshift;
|
|
95
|
+
const root = j(file.source);
|
|
96
|
+
|
|
97
|
+
if (!importsCoreBanner(j, root)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let hasChanges = false;
|
|
102
|
+
|
|
103
|
+
/** `collapsible={{defaultIsOpen: <expr>}}` */
|
|
104
|
+
const collapsibleWithDefault = (/** @type {any} */ expression) =>
|
|
105
|
+
j.jsxAttribute(
|
|
106
|
+
j.jsxIdentifier(NEW_PROP),
|
|
107
|
+
j.jsxExpressionContainer(
|
|
108
|
+
j.objectExpression([
|
|
109
|
+
j.objectProperty(j.identifier('defaultIsOpen'), expression),
|
|
110
|
+
]),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
root.find(j.JSXOpeningElement).forEach((/** @type {any} */ path) => {
|
|
115
|
+
const name = path.node.name;
|
|
116
|
+
if (name?.type !== 'JSXIdentifier' || name.name !== COMPONENT) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const attrs = path.node.attributes ?? [];
|
|
121
|
+
const oldIndex = attrs.findIndex(
|
|
122
|
+
(/** @type {any} */ a) =>
|
|
123
|
+
a.type === 'JSXAttribute' && a.name?.name === OLD_PROP,
|
|
124
|
+
);
|
|
125
|
+
if (oldIndex === -1) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Already migrated by hand, or a spread that may carry either prop:
|
|
130
|
+
// in both cases a rewrite would be guesswork.
|
|
131
|
+
const hasNewProp = attrs.some(
|
|
132
|
+
(/** @type {any} */ a) =>
|
|
133
|
+
a.type === 'JSXAttribute' && a.name?.name === NEW_PROP,
|
|
134
|
+
);
|
|
135
|
+
const hasSpread = attrs.some(
|
|
136
|
+
(/** @type {any} */ a) => a.type === 'JSXSpreadAttribute',
|
|
137
|
+
);
|
|
138
|
+
if (hasNewProp || hasSpread) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const value = attrs[oldIndex].value;
|
|
143
|
+
|
|
144
|
+
if (value == null) {
|
|
145
|
+
// Bare `defaultIsExpanded` — starts open.
|
|
146
|
+
attrs[oldIndex] = collapsibleWithDefault(j.booleanLiteral(true));
|
|
147
|
+
} else if (value.type === 'JSXExpressionContainer') {
|
|
148
|
+
const expression = value.expression;
|
|
149
|
+
const isBooleanLiteral =
|
|
150
|
+
expression.type === 'BooleanLiteral' ||
|
|
151
|
+
(expression.type === 'Literal' && typeof expression.value === 'boolean');
|
|
152
|
+
if (isBooleanLiteral && expression.value === false) {
|
|
153
|
+
// Starting closed is the default now, so the prop simply goes.
|
|
154
|
+
attrs.splice(oldIndex, 1);
|
|
155
|
+
} else if (isBooleanLiteral) {
|
|
156
|
+
attrs[oldIndex] = collapsibleWithDefault(j.booleanLiteral(true));
|
|
157
|
+
} else {
|
|
158
|
+
// A dynamic default stays dynamic.
|
|
159
|
+
attrs[oldIndex] = collapsibleWithDefault(expression);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// `defaultIsExpanded="something"` is not valid for a boolean prop;
|
|
163
|
+
// leave it for a human rather than inventing a meaning.
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
hasChanges = true;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return hasChanges ? root.toSource({quote: 'single'}) : undefined;
|
|
171
|
+
}
|
|
@@ -7,4 +7,14 @@
|
|
|
7
7
|
* this file into the resolved version folder.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import bannerCollapsibleContent, {
|
|
11
|
+
meta as bannerCollapsibleContentMeta,
|
|
12
|
+
} from './banner-collapsible-content.mjs';
|
|
13
|
+
|
|
14
|
+
export default [
|
|
15
|
+
{
|
|
16
|
+
name: 'banner-collapsible-content',
|
|
17
|
+
transform: bannerCollapsibleContent,
|
|
18
|
+
meta: bannerCollapsibleContentMeta,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
@@ -6,7 +6,7 @@ export const doc = {
|
|
|
6
6
|
exampleFor: 'Banner',
|
|
7
7
|
name: 'Banner — Collapsible',
|
|
8
8
|
displayName: 'Banner — Collapsible',
|
|
9
|
-
description: 'Combine an action button, dismiss control, and
|
|
9
|
+
description: 'Combine an action button, dismiss control, and a collapsible detail area in one banner. Children sit behind the toggle by default; `collapsible={{defaultIsOpen: true}}` starts it open, and `collapsible={false}` drops the toggle entirely. Use for complex notifications like config changes or deployment summaries.',
|
|
10
10
|
isReady: true,
|
|
11
11
|
aspectRatio: 16 / 9,
|
|
12
12
|
componentsUsed: ['Banner', 'Button', 'List', 'Layout', 'Text'],
|
|
@@ -16,7 +16,7 @@ export default function BannerCollapsibleContent() {
|
|
|
16
16
|
description="Review the changes before they take effect."
|
|
17
17
|
endContent={<Button label="Review" variant="secondary" size="sm" />}
|
|
18
18
|
isDismissable
|
|
19
|
-
|
|
19
|
+
collapsible={{defaultIsOpen: true}}>
|
|
20
20
|
<Stack direction="vertical" gap={2}>
|
|
21
21
|
<Text type="supporting" color="secondary">
|
|
22
22
|
Changed settings:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.4.7-canary.
|
|
3
|
+
"version": "0.4.7-canary.5da424d",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
"zod": "^4.4.3"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@astryxdesign/charts": "0.4.7-canary.
|
|
91
|
-
"@astryxdesign/core": "0.4.7-canary.
|
|
92
|
-
"@astryxdesign/lab": "0.4.7-canary.
|
|
93
|
-
"@astryxdesign/theme-neutral": "0.4.7-canary.
|
|
90
|
+
"@astryxdesign/charts": "0.4.7-canary.5da424d",
|
|
91
|
+
"@astryxdesign/core": "0.4.7-canary.5da424d",
|
|
92
|
+
"@astryxdesign/lab": "0.4.7-canary.5da424d",
|
|
93
|
+
"@astryxdesign/theme-neutral": "0.4.7-canary.5da424d",
|
|
94
94
|
"gpt-tokenizer": "^3.4.0"
|
|
95
95
|
},
|
|
96
96
|
"peerDependenciesMeta": {
|
|
@@ -108,10 +108,10 @@
|
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@astryxdesign/charts": "0.4.7-canary.
|
|
112
|
-
"@astryxdesign/core": "0.4.7-canary.
|
|
113
|
-
"@astryxdesign/lab": "0.4.7-canary.
|
|
114
|
-
"@astryxdesign/theme-neutral": "0.4.7-canary.
|
|
111
|
+
"@astryxdesign/charts": "0.4.7-canary.5da424d",
|
|
112
|
+
"@astryxdesign/core": "0.4.7-canary.5da424d",
|
|
113
|
+
"@astryxdesign/lab": "0.4.7-canary.5da424d",
|
|
114
|
+
"@astryxdesign/theme-neutral": "0.4.7-canary.5da424d",
|
|
115
115
|
"gpt-tokenizer": "^3.4.0"
|
|
116
116
|
},
|
|
117
117
|
"scripts": {
|