@astryxdesign/cli 0.3.0-canary.fc1c3f3 → 0.3.0-canary.fcbc342
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 +8 -8
- package/api/blog/blog.doc.mjs +2 -2
- package/api/build/build.doc.mjs +1 -1
- package/api/component/component.doc.mjs +1 -1
- package/api/discover/discover.doc.mjs +2 -2
- package/api/docs/docs.doc.mjs +2 -2
- package/api/doctor/doctor.doc.mjs +3 -3
- package/api/hook/hook.doc.mjs +1 -1
- package/api/init/init.doc.mjs +1 -1
- package/api/integration/summarizeIssues.doc.mjs +1 -1
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/assertResponse.doc.mjs +2 -2
- package/api/json/isError.doc.mjs +1 -1
- package/api/json/parseResponse.doc.mjs +2 -2
- package/api/layout/layoutCheck.doc.mjs +1 -1
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/layout/layoutGrammar.doc.mjs +1 -1
- package/api/swizzle/swizzle.doc.mjs +1 -1
- package/api/template/template.test.mjs +26 -0
- package/api/theme/listThemes.doc.mjs +2 -2
- package/api/theme/themeAdd.doc.mjs +1 -1
- package/api/theme/themeBuild.doc.mjs +3 -3
- package/api/theme/themeList.doc.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +1 -1
- package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +92 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/rename-dropdown-menu-radio-dot-target.mjs +152 -0
- package/assets/codemods/transforms/v0.3.0/__tests__/migrate-table-rowexpansion-to-tree.test.mjs +201 -0
- package/assets/codemods/transforms/v0.3.0/index.mjs +8 -0
- package/assets/codemods/transforms/v0.3.0/migrate-table-rowexpansion-to-tree.mjs +214 -0
- package/assets/docs/browser-support.doc.mjs +6 -6
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/icons.doc.mjs +3 -3
- package/assets/docs/illustrations.doc.mjs +2 -2
- package/assets/docs/internationalization.doc.mjs +1 -1
- package/assets/docs/migration.doc.mjs +1 -1
- package/assets/docs/principles.doc.mjs +1 -1
- package/assets/docs/styling-libraries.doc.mjs +1 -1
- package/assets/docs/styling.doc.mjs +4 -4
- package/assets/docs/theme.doc.mjs +3 -3
- package/assets/docs/typography.doc.mjs +2 -2
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +24 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +68 -0
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +92 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +41 -0
- package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +25 -0
- package/assets/templates/blocks/components/Stepper/StepperStatus.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +50 -0
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +40 -0
- package/assets/templates/blocks/components/Table/TableRowExpansionTable.tsx +61 -58
- package/assets/templates/blocks/components/TextArea/TextAreaStates.tsx +1 -1
- package/assets/templates/themes/neutral/neutralTheme.ts +4 -1
- package/authoring/codemod/codemod.doc.mjs +2 -2
- package/authoring/config/config.doc.mjs +1 -1
- package/authoring/doctypes/base/type.ts +15 -0
- package/authoring/doctypes/command/command.doc.mjs +2 -2
- package/authoring/doctypes/component/component.doc.mjs +7 -7
- package/authoring/doctypes/enum/enum.doc.mjs +2 -2
- package/authoring/doctypes/function/function.doc.mjs +3 -3
- package/authoring/doctypes/hook/hook.doc.mjs +3 -3
- package/authoring/doctypes/reference/reference.doc.mjs +3 -3
- package/authoring/doctypes/schema/schema.doc.mjs +1 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- package/clients/cli/commands/build-theme.registry.test.mjs +14 -1
- package/clients/cli/commands/build.doc.mjs +1 -1
- package/clients/cli/commands/component.doc.mjs +1 -1
- package/clients/cli/commands/doctor.doc.mjs +2 -2
- package/clients/cli/commands/layout-expand.doc.mjs +1 -1
- package/clients/cli/commands/layout.doc.mjs +1 -1
- package/clients/cli/commands/manifest.doc.mjs +3 -3
- package/clients/cli/commands/search.doc.mjs +1 -1
- package/clients/cli/commands/theme-add.doc.mjs +1 -1
- package/clients/cli/commands/theme-build.doc.mjs +1 -1
- package/clients/cli/commands/theme-list.doc.mjs +1 -1
- package/clients/cli/commands/theme.doc.mjs +2 -2
- package/clients/cli/commands/validate-integration.doc.mjs +2 -2
- package/foundation/discovery/template-adapter.d.mts +6 -3
- package/foundation/discovery/template-adapter.mjs +30 -14
- package/foundation/response/response-types.doc.mjs +7 -7
- package/foundation/response/response.doc.mjs +5 -5
- package/package.json +9 -9
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: rename the removed `dropdown-menu-radio-dot` theme target
|
|
5
|
+
*
|
|
6
|
+
* The menu radio's dot is no longer drawn by DropdownMenuRadioItem. That row
|
|
7
|
+
* now renders the shared radio indicator, so its dot is the indicator's dot and
|
|
8
|
+
* carries `radio-indicator-dot` (plus the legacy `radio-dot`) instead of the
|
|
9
|
+
* menu-specific `dropdown-menu-radio-dot`, which is gone.
|
|
10
|
+
*
|
|
11
|
+
* Runtime themes are not validated: a theme keyed on the old target keeps
|
|
12
|
+
* compiling and simply stops matching, with no error anywhere. That silence is
|
|
13
|
+
* why this rename needs a codemod rather than a changelog line.
|
|
14
|
+
*
|
|
15
|
+
* The rename is NOT scope-preserving, and that cannot be fixed here — there is
|
|
16
|
+
* no menu-only dot element left to address. `radio-indicator-dot` reaches every
|
|
17
|
+
* radio dot in the app, including RadioList's. So every rewritten site also
|
|
18
|
+
* gets a TODO comment (api.report is a stub; comments are the only warning
|
|
19
|
+
* channel) telling the author to check whether the rule was meant to be
|
|
20
|
+
* menu-only, and pointing at the containing-target route if it was.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const meta = {
|
|
24
|
+
title: 'Rename the removed dropdown-menu-radio-dot theme target',
|
|
25
|
+
description:
|
|
26
|
+
'Renames the `dropdown-menu-radio-dot` theme target (and the ' +
|
|
27
|
+
'`astryx-dropdown-menu-radio-dot` class it rendered) to ' +
|
|
28
|
+
'`radio-indicator-dot` / `astryx-radio-indicator-dot`. Menu radios draw ' +
|
|
29
|
+
'the shared radio indicator now, so the menu-specific dot target no ' +
|
|
30
|
+
'longer exists. The new target is app-wide rather than menu-only, so each ' +
|
|
31
|
+
'rewritten site gets a TODO comment to confirm that widening is intended.',
|
|
32
|
+
pr: '#4712',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const OLD_TARGET = 'dropdown-menu-radio-dot';
|
|
36
|
+
const NEW_TARGET = 'radio-indicator-dot';
|
|
37
|
+
const OLD_CLASS = `astryx-${OLD_TARGET}`;
|
|
38
|
+
const NEW_CLASS = `astryx-${NEW_TARGET}`;
|
|
39
|
+
|
|
40
|
+
const TODO_COMMENT =
|
|
41
|
+
' TODO(astryx upgrade): `dropdown-menu-radio-dot` became `radio-indicator-dot`,' +
|
|
42
|
+
' which styles EVERY radio dot, not just the ones in a menu. If this rule was' +
|
|
43
|
+
' meant to be menu-only, scope it under the containing `dropdown-menu-radio`' +
|
|
44
|
+
' target instead of this one. ';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rewrite one string value, or return null when it holds nothing to rename.
|
|
48
|
+
*
|
|
49
|
+
* Handles the target name on its own (a theme's `components` key) and the
|
|
50
|
+
* rendered class inside a larger string (a selector such as
|
|
51
|
+
* `.astryx-dropdown-menu-radio-dot`, or a className list).
|
|
52
|
+
*
|
|
53
|
+
* @param {string} value
|
|
54
|
+
* @returns {string | null}
|
|
55
|
+
*/
|
|
56
|
+
function renameIn(value) {
|
|
57
|
+
if (value === OLD_TARGET) {
|
|
58
|
+
return NEW_TARGET;
|
|
59
|
+
}
|
|
60
|
+
if (value.includes(OLD_CLASS)) {
|
|
61
|
+
return value.split(OLD_CLASS).join(NEW_CLASS);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
|
|
68
|
+
* @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
|
|
69
|
+
* @returns {string | null | undefined}
|
|
70
|
+
*/
|
|
71
|
+
export default function transformer(file, api) {
|
|
72
|
+
// Cheap bail-out: most files in a consumer repo mention neither name.
|
|
73
|
+
if (!file.source.includes(OLD_TARGET)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const j = api.jscodeshift;
|
|
78
|
+
const root = j(file.source);
|
|
79
|
+
let hasChanges = false;
|
|
80
|
+
|
|
81
|
+
/** Attach the widening warning once to the nearest statement-ish node. */
|
|
82
|
+
function attachTodo(/** @type {any} */ path) {
|
|
83
|
+
// The property (or its statement) reads better than the bare literal, and
|
|
84
|
+
// matches where a human would look for the note.
|
|
85
|
+
const host =
|
|
86
|
+
path.parent?.node?.type === 'ObjectProperty' ||
|
|
87
|
+
path.parent?.node?.type === 'Property'
|
|
88
|
+
? path.parent.node
|
|
89
|
+
: path.node;
|
|
90
|
+
if (!host.comments) {
|
|
91
|
+
host.comments = [];
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
host.comments.some((/** @type {any} */ c) => c.value === TODO_COMMENT)
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
host.comments.push(j.commentBlock(TODO_COMMENT, true, false));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
root
|
|
102
|
+
.find(j.StringLiteral)
|
|
103
|
+
.forEach((/** @type {any} */ path) => {
|
|
104
|
+
const renamed = renameIn(path.node.value);
|
|
105
|
+
if (renamed == null) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
path.node.value = renamed;
|
|
109
|
+
attachTodo(path);
|
|
110
|
+
hasChanges = true;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Older parsers surface string literals as `Literal`.
|
|
114
|
+
root.find(j.Literal).forEach((/** @type {any} */ path) => {
|
|
115
|
+
if (typeof path.node.value !== 'string') {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const renamed = renameIn(path.node.value);
|
|
119
|
+
if (renamed == null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
path.node.value = renamed;
|
|
123
|
+
if (typeof path.node.raw === 'string') {
|
|
124
|
+
path.node.raw = path.node.raw
|
|
125
|
+
.split(OLD_TARGET)
|
|
126
|
+
.join(NEW_TARGET);
|
|
127
|
+
}
|
|
128
|
+
attachTodo(path);
|
|
129
|
+
hasChanges = true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Template literals carry the class in CSS strings: `.${OLD_CLASS} > span`.
|
|
133
|
+
root.find(j.TemplateElement).forEach((/** @type {any} */ path) => {
|
|
134
|
+
const cooked = path.node.value?.cooked;
|
|
135
|
+
if (typeof cooked !== 'string') {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const renamed = renameIn(cooked);
|
|
139
|
+
if (renamed == null) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
path.node.value.cooked = renamed;
|
|
143
|
+
path.node.value.raw = path.node.value.raw
|
|
144
|
+
.split(OLD_CLASS)
|
|
145
|
+
.join(NEW_CLASS)
|
|
146
|
+
.split(OLD_TARGET)
|
|
147
|
+
.join(NEW_TARGET);
|
|
148
|
+
hasChanges = true;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return hasChanges ? root.toSource({quote: 'single'}) : undefined;
|
|
152
|
+
}
|
package/assets/codemods/transforms/v0.3.0/__tests__/migrate-table-rowexpansion-to-tree.test.mjs
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
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(
|
|
7
|
+
'../migrate-table-rowexpansion-to-tree.mjs'
|
|
8
|
+
);
|
|
9
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
10
|
+
const j = jscodeshift.withParser('tsx');
|
|
11
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
12
|
+
const file = {source, path: 'test.tsx'};
|
|
13
|
+
const result = transform(file, api);
|
|
14
|
+
return result ?? source;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalize(str) {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.replace(/\{\s+/g, '{')
|
|
21
|
+
.replace(/\s+\}/g, '}')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('migrate-table-rowexpansion-to-tree', () => {
|
|
26
|
+
it('rewrites the useTableRowExpansionState import to useTableTreeState', async () => {
|
|
27
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
28
|
+
function C() {
|
|
29
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
30
|
+
baseData: tree,
|
|
31
|
+
getChildren: item => item.children ?? [],
|
|
32
|
+
getRowKey: item => item.id,
|
|
33
|
+
expandedKeys,
|
|
34
|
+
setExpandedKeys,
|
|
35
|
+
});
|
|
36
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
37
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
38
|
+
}`;
|
|
39
|
+
const output = await applyTransform(input);
|
|
40
|
+
expect(output).not.toContain('useTableRowExpansionState');
|
|
41
|
+
expect(output).toContain('useTableTreeState');
|
|
42
|
+
expect(output).toContain('useTableTreeData');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('maps baseData to data and getChildren to childrenKey', async () => {
|
|
46
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
47
|
+
function C() {
|
|
48
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
49
|
+
baseData: tree,
|
|
50
|
+
getChildren: item => item.children ?? [],
|
|
51
|
+
getRowKey: item => item.id,
|
|
52
|
+
expandedKeys,
|
|
53
|
+
setExpandedKeys,
|
|
54
|
+
});
|
|
55
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
56
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
57
|
+
}`;
|
|
58
|
+
const output = await applyTransform(input);
|
|
59
|
+
const n = normalize(output);
|
|
60
|
+
expect(n).toContain('data: tree');
|
|
61
|
+
expect(n).toContain("childrenKey: 'children'");
|
|
62
|
+
expect(output).not.toContain('baseData');
|
|
63
|
+
expect(output).not.toContain('getChildren');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('maps getRowKey to idKey preserving the accessor function', async () => {
|
|
67
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
68
|
+
function C() {
|
|
69
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
70
|
+
baseData: tree,
|
|
71
|
+
getChildren: item => item.children ?? [],
|
|
72
|
+
getRowKey: item => item.id,
|
|
73
|
+
expandedKeys,
|
|
74
|
+
setExpandedKeys,
|
|
75
|
+
});
|
|
76
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
77
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
78
|
+
}`;
|
|
79
|
+
const output = await applyTransform(input);
|
|
80
|
+
const n = normalize(output);
|
|
81
|
+
expect(n).toContain('idKey: item => item.id');
|
|
82
|
+
expect(output).not.toContain('getRowKey');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('swaps the useTableRowExpansion plugin call for useTableTreeData', async () => {
|
|
86
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
87
|
+
function C() {
|
|
88
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
89
|
+
baseData: tree,
|
|
90
|
+
getChildren: item => item.children ?? [],
|
|
91
|
+
getRowKey: item => item.id,
|
|
92
|
+
expandedKeys,
|
|
93
|
+
setExpandedKeys,
|
|
94
|
+
});
|
|
95
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
96
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
97
|
+
}`;
|
|
98
|
+
const output = await applyTransform(input);
|
|
99
|
+
// The state hook now returns treeConfig; the plugin consumes it.
|
|
100
|
+
expect(output).toContain('treeConfig');
|
|
101
|
+
expect(output).toContain('useTableTreeData(treeConfig)');
|
|
102
|
+
expect(output).not.toMatch(/useTableRowExpansion\(/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('maps getIsItemExpandable to isItemExpandable', async () => {
|
|
106
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
107
|
+
function C() {
|
|
108
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
109
|
+
baseData: tree,
|
|
110
|
+
getChildren: item => item.children ?? [],
|
|
111
|
+
getRowKey: item => item.id,
|
|
112
|
+
getIsItemExpandable: item => item.type === 'folder',
|
|
113
|
+
expandedKeys,
|
|
114
|
+
setExpandedKeys,
|
|
115
|
+
});
|
|
116
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
117
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
118
|
+
}`;
|
|
119
|
+
const output = await applyTransform(input);
|
|
120
|
+
expect(output).toContain('isItemExpandable');
|
|
121
|
+
expect(output).not.toContain('getIsItemExpandable');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('leaves the new detail-panel usage (renderExpanded) untouched', async () => {
|
|
125
|
+
const input = `import {Table, useTableRowExpansion} from '@astryxdesign/core';
|
|
126
|
+
function C() {
|
|
127
|
+
const expansion = useTableRowExpansion({
|
|
128
|
+
expandedKeys,
|
|
129
|
+
onToggle,
|
|
130
|
+
getRowKey: item => item.id,
|
|
131
|
+
renderExpanded: item => <Details item={item} />,
|
|
132
|
+
});
|
|
133
|
+
return <Table data={rows} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
134
|
+
}`;
|
|
135
|
+
const output = await applyTransform(input);
|
|
136
|
+
// No useTableRowExpansionState import here, and renderExpanded present:
|
|
137
|
+
// this is the new detail-panel API. The codemod must not touch it.
|
|
138
|
+
expect(output).toBe(input);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('does not touch files that never imported the row-expansion hooks', async () => {
|
|
142
|
+
const input = `import {Table, useTableSelection} from '@astryxdesign/core';
|
|
143
|
+
const t = <Table data={rows} columns={cols} idKey="id" />;`;
|
|
144
|
+
const output = await applyTransform(input);
|
|
145
|
+
expect(output).toBe(input);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('supports the @xds/core import source alias', async () => {
|
|
149
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@xds/core';
|
|
150
|
+
function C() {
|
|
151
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
152
|
+
baseData: tree,
|
|
153
|
+
getChildren: item => item.children ?? [],
|
|
154
|
+
getRowKey: item => item.id,
|
|
155
|
+
expandedKeys,
|
|
156
|
+
setExpandedKeys,
|
|
157
|
+
});
|
|
158
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
159
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
160
|
+
}`;
|
|
161
|
+
const output = await applyTransform(input);
|
|
162
|
+
expect(output).toContain('useTableTreeState');
|
|
163
|
+
expect(output).not.toContain('useTableRowExpansionState');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('emits treeConfig as a shorthand (not treeConfig: treeConfig)', async () => {
|
|
167
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
168
|
+
function C() {
|
|
169
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
170
|
+
baseData: tree,
|
|
171
|
+
getChildren: item => item.children ?? [],
|
|
172
|
+
getRowKey: item => item.id,
|
|
173
|
+
expandedKeys,
|
|
174
|
+
setExpandedKeys,
|
|
175
|
+
});
|
|
176
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
177
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
178
|
+
}`;
|
|
179
|
+
const output = await applyTransform(input);
|
|
180
|
+
expect(output).not.toContain('treeConfig: treeConfig');
|
|
181
|
+
expect(normalize(output)).toContain('visibleData: data');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('leaves a migration guidance comment about expansion state', async () => {
|
|
185
|
+
const input = `import {Table, useTableRowExpansion, useTableRowExpansionState} from '@astryxdesign/core';
|
|
186
|
+
function C() {
|
|
187
|
+
const {data, expansionConfig} = useTableRowExpansionState({
|
|
188
|
+
baseData: tree,
|
|
189
|
+
getChildren: item => item.children ?? [],
|
|
190
|
+
getRowKey: item => item.id,
|
|
191
|
+
expandedKeys,
|
|
192
|
+
setExpandedKeys,
|
|
193
|
+
});
|
|
194
|
+
const expansion = useTableRowExpansion(expansionConfig);
|
|
195
|
+
return <Table data={data} columns={cols} idKey="id" plugins={{expansion}} />;
|
|
196
|
+
}`;
|
|
197
|
+
const output = await applyTransform(input);
|
|
198
|
+
expect(output).toContain('astryx-migration');
|
|
199
|
+
expect(output).toContain('defaultExpandedIds');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -40,6 +40,9 @@ import migrateLabCodeBlockImports, {
|
|
|
40
40
|
import removeThemeTransitionTokenImports, {
|
|
41
41
|
meta as removeThemeTransitionTokenImportsMeta,
|
|
42
42
|
} from './remove-theme-transition-token-imports.mjs';
|
|
43
|
+
import migrateTableRowExpansionToTree, {
|
|
44
|
+
meta as migrateTableRowExpansionToTreeMeta,
|
|
45
|
+
} from './migrate-table-rowexpansion-to-tree.mjs';
|
|
43
46
|
|
|
44
47
|
export default [
|
|
45
48
|
{
|
|
@@ -87,4 +90,9 @@ export default [
|
|
|
87
90
|
transform: removeThemeTransitionTokenImports,
|
|
88
91
|
meta: removeThemeTransitionTokenImportsMeta,
|
|
89
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'migrate-table-rowexpansion-to-tree',
|
|
95
|
+
transform: migrateTableRowExpansionToTree,
|
|
96
|
+
meta: migrateTableRowExpansionToTreeMeta,
|
|
97
|
+
},
|
|
90
98
|
];
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Migrate tree-mode useTableRowExpansion to the tree plugin
|
|
5
|
+
*
|
|
6
|
+
* useTableRowExpansion is now a detail-panel plugin (renderExpanded). Its old
|
|
7
|
+
* tree mode (child rows that reuse the parent columns) moved to
|
|
8
|
+
* useTableTreeData + useTableTreeState, and useTableRowExpansionState was
|
|
9
|
+
* removed. This codemod rewrites the standard tree pattern:
|
|
10
|
+
*
|
|
11
|
+
* const {data, expansionConfig} = useTableRowExpansionState({
|
|
12
|
+
* baseData, getChildren, getRowKey, expandedKeys, setExpandedKeys,
|
|
13
|
+
* });
|
|
14
|
+
* const expansion = useTableRowExpansion(expansionConfig);
|
|
15
|
+
*
|
|
16
|
+
* into:
|
|
17
|
+
*
|
|
18
|
+
* const {visibleData: data, treeConfig} = useTableTreeState({
|
|
19
|
+
* data: baseData, childrenKey: 'children', idKey: getRowKey, ...
|
|
20
|
+
* });
|
|
21
|
+
* const expansion = useTableTreeData(treeConfig);
|
|
22
|
+
*
|
|
23
|
+
* Files that use the new detail-panel API (renderExpanded, no
|
|
24
|
+
* useTableRowExpansionState import) are left untouched.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const meta = {
|
|
28
|
+
title: 'Migrate tree-mode useTableRowExpansion to the tree plugin',
|
|
29
|
+
description:
|
|
30
|
+
'Rewrites the removed useTableRowExpansionState tree pattern to useTableTreeState + useTableTreeData. Detail-panel usage (renderExpanded) is left untouched.',
|
|
31
|
+
pr: '#4612',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const IMPORT_SOURCES = new Set([
|
|
35
|
+
'@astryxdesign/core',
|
|
36
|
+
'@astryxdesign/core/Table',
|
|
37
|
+
'@xds/core',
|
|
38
|
+
'@xds/core/Table',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
|
|
43
|
+
* @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
|
|
44
|
+
* @returns {string | null | undefined}
|
|
45
|
+
*/
|
|
46
|
+
export default function transformer(file, api) {
|
|
47
|
+
const j = api.jscodeshift;
|
|
48
|
+
const root = j(file.source);
|
|
49
|
+
|
|
50
|
+
// Only act on files importing the removed state hook. Detail-panel-only
|
|
51
|
+
// usage (useTableRowExpansion alone) is the new API and must be left alone.
|
|
52
|
+
let stateLocal = null;
|
|
53
|
+
let pluginLocal = null;
|
|
54
|
+
/** @type {any[]} */
|
|
55
|
+
const importPaths = [];
|
|
56
|
+
root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
|
|
57
|
+
if (!IMPORT_SOURCES.has(path.node.source.value)) return;
|
|
58
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
59
|
+
if (spec.type !== 'ImportSpecifier') continue;
|
|
60
|
+
if (spec.imported.name === 'useTableRowExpansionState') {
|
|
61
|
+
stateLocal = spec.local?.name ?? spec.imported.name;
|
|
62
|
+
importPaths.push(path);
|
|
63
|
+
}
|
|
64
|
+
if (spec.imported.name === 'useTableRowExpansion') {
|
|
65
|
+
pluginLocal = spec.local?.name ?? spec.imported.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (!stateLocal) return undefined;
|
|
70
|
+
|
|
71
|
+
let hasChanges = false;
|
|
72
|
+
|
|
73
|
+
// --- 1. Rewrite the state hook call + its config object ---
|
|
74
|
+
root
|
|
75
|
+
.find(j.CallExpression, {callee: {name: stateLocal}})
|
|
76
|
+
.forEach((/** @type {any} */ path) => {
|
|
77
|
+
const arg = path.node.arguments[0];
|
|
78
|
+
if (!arg || arg.type !== 'ObjectExpression') return;
|
|
79
|
+
|
|
80
|
+
/** @type {any} */
|
|
81
|
+
let getChildrenValue = null;
|
|
82
|
+
const nextProps = [];
|
|
83
|
+
for (const prop of arg.properties) {
|
|
84
|
+
const key =
|
|
85
|
+
prop.key?.name ?? (prop.key?.value ? String(prop.key.value) : null);
|
|
86
|
+
if (key === 'baseData') {
|
|
87
|
+
nextProps.push(j.property('init', j.identifier('data'), prop.value));
|
|
88
|
+
} else if (key === 'getChildren') {
|
|
89
|
+
getChildrenValue = prop.value;
|
|
90
|
+
// childrenKey is emitted below (default 'children').
|
|
91
|
+
} else if (key === 'getRowKey') {
|
|
92
|
+
nextProps.push(j.property('init', j.identifier('idKey'), prop.value));
|
|
93
|
+
} else if (key === 'getIsItemExpandable') {
|
|
94
|
+
nextProps.push(
|
|
95
|
+
j.property('init', j.identifier('isItemExpandable'), prop.value),
|
|
96
|
+
);
|
|
97
|
+
} else if (key === 'expandedKeys' || key === 'setExpandedKeys') {
|
|
98
|
+
// The tree state hook owns expansion internally (uncontrolled) or
|
|
99
|
+
// via expandedIds/onExpandedIdsChange (controlled). The 1:1
|
|
100
|
+
// controlled mapping is not mechanical, so these are dropped and a
|
|
101
|
+
// guidance comment is attached below.
|
|
102
|
+
continue;
|
|
103
|
+
} else {
|
|
104
|
+
nextProps.push(prop);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// childrenKey: derive the literal when getChildren is the canonical
|
|
109
|
+
// `item => item.children` / `item.children ?? []`; otherwise keep the
|
|
110
|
+
// default and let the guidance comment flag it.
|
|
111
|
+
nextProps.splice(
|
|
112
|
+
1,
|
|
113
|
+
0,
|
|
114
|
+
j.property('init', j.identifier('childrenKey'), j.literal('children')),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
arg.properties = nextProps;
|
|
118
|
+
path.node.callee = j.identifier('useTableTreeState');
|
|
119
|
+
hasChanges = true;
|
|
120
|
+
|
|
121
|
+
// Rename the destructured result: data -> visibleData (aliased back to
|
|
122
|
+
// the local name), expansionConfig -> treeConfig.
|
|
123
|
+
const declarator = path.parent.node;
|
|
124
|
+
if (
|
|
125
|
+
declarator.type === 'VariableDeclarator' &&
|
|
126
|
+
declarator.id.type === 'ObjectPattern'
|
|
127
|
+
) {
|
|
128
|
+
for (const p of declarator.id.properties) {
|
|
129
|
+
if (p.type !== 'ObjectProperty' && p.type !== 'Property') continue;
|
|
130
|
+
const kName = p.key?.name;
|
|
131
|
+
if (kName === 'data') {
|
|
132
|
+
// {data} -> {visibleData: data}
|
|
133
|
+
p.key = j.identifier('visibleData');
|
|
134
|
+
if (p.shorthand) {
|
|
135
|
+
p.shorthand = false;
|
|
136
|
+
p.value = j.identifier('data');
|
|
137
|
+
}
|
|
138
|
+
} else if (kName === 'expansionConfig') {
|
|
139
|
+
// {expansionConfig} -> {treeConfig} (keep it shorthand).
|
|
140
|
+
p.key = j.identifier('treeConfig');
|
|
141
|
+
if (p.value?.type === 'Identifier') {
|
|
142
|
+
p.value = j.identifier('treeConfig');
|
|
143
|
+
p.shorthand = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Attach a one-line migration note as a leading comment on the
|
|
150
|
+
// statement, so a human confirms the expansion-state wiring.
|
|
151
|
+
const stmt = path.parent.parent.node;
|
|
152
|
+
if (stmt && !stmt.comments) {
|
|
153
|
+
stmt.comments = [
|
|
154
|
+
j.commentLine(
|
|
155
|
+
' astryx-migration: verify tree expansion state. useTableTreeState',
|
|
156
|
+
true,
|
|
157
|
+
false,
|
|
158
|
+
),
|
|
159
|
+
j.commentLine(
|
|
160
|
+
' is uncontrolled by default; pass defaultExpandedIds, or',
|
|
161
|
+
true,
|
|
162
|
+
false,
|
|
163
|
+
),
|
|
164
|
+
j.commentLine(
|
|
165
|
+
' expandedIds + onExpandedIdsChange for the old controlled set.',
|
|
166
|
+
true,
|
|
167
|
+
false,
|
|
168
|
+
),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
void getChildrenValue;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// --- 2. Swap the plugin call: useTableRowExpansion(cfg) -> useTableTreeData(cfg) ---
|
|
175
|
+
if (pluginLocal) {
|
|
176
|
+
root
|
|
177
|
+
.find(j.CallExpression, {callee: {name: pluginLocal}})
|
|
178
|
+
.forEach((/** @type {any} */ path) => {
|
|
179
|
+
// Rename the argument identifier expansionConfig -> treeConfig.
|
|
180
|
+
const a = path.node.arguments[0];
|
|
181
|
+
if (a && a.type === 'Identifier' && a.name === 'expansionConfig') {
|
|
182
|
+
path.node.arguments[0] = j.identifier('treeConfig');
|
|
183
|
+
}
|
|
184
|
+
path.node.callee = j.identifier('useTableTreeData');
|
|
185
|
+
hasChanges = true;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!hasChanges) return undefined;
|
|
190
|
+
|
|
191
|
+
// --- 3. Fix imports: drop the removed hooks, add the tree hooks ---
|
|
192
|
+
for (const path of importPaths) {
|
|
193
|
+
const specs = path.node.specifiers.filter(
|
|
194
|
+
(/** @type {any} */ s) =>
|
|
195
|
+
!(
|
|
196
|
+
s.type === 'ImportSpecifier' &&
|
|
197
|
+
(s.imported.name === 'useTableRowExpansionState' ||
|
|
198
|
+
s.imported.name === 'useTableRowExpansion')
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
const existing = new Set(
|
|
202
|
+
specs.map((/** @type {any} */ s) => s.imported?.name),
|
|
203
|
+
);
|
|
204
|
+
if (!existing.has('useTableTreeState')) {
|
|
205
|
+
specs.push(j.importSpecifier(j.identifier('useTableTreeState')));
|
|
206
|
+
}
|
|
207
|
+
if (!existing.has('useTableTreeData')) {
|
|
208
|
+
specs.push(j.importSpecifier(j.identifier('useTableTreeData')));
|
|
209
|
+
}
|
|
210
|
+
path.node.specifiers = specs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return root.toSource({quote: 'single'});
|
|
214
|
+
}
|
|
@@ -15,7 +15,7 @@ export const docs = {
|
|
|
15
15
|
content: [
|
|
16
16
|
{
|
|
17
17
|
type: 'prose',
|
|
18
|
-
text: 'Astryx is built on modern web platform features: the Popover API, CSS anchor positioning, and CSS `light-dark()`. These let components stay small, accessible, and dependency-free, but they also set a floor on which browsers render everything correctly.',
|
|
18
|
+
text: 'Astryx is built on modern web platform features: the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API, CSS anchor positioning, and CSS `light-dark()`. These let components stay small, accessible, and dependency-free, but they also set a floor on which browsers render everything correctly.',
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
type: 'prose',
|
|
@@ -73,7 +73,7 @@ export const docs = {
|
|
|
73
73
|
'Baseline 2026: the tightest requirement.',
|
|
74
74
|
],
|
|
75
75
|
[
|
|
76
|
-
'Popover API',
|
|
76
|
+
'[`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API',
|
|
77
77
|
'Opens, stacks, and light-dismisses layered surfaces via the top layer.',
|
|
78
78
|
'Baseline 2025.',
|
|
79
79
|
],
|
|
@@ -86,7 +86,7 @@ export const docs = {
|
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
type: 'prose',
|
|
89
|
-
text: 'The gap that matters is between Tier 1 and Tier 2: the Popover API and `light-dark()` reached wide availability well before anchor positioning. So in Tier 2 browsers, layered surfaces open and dismiss correctly; they just are not positioned. This is the one feature most consumers will need to reason about.',
|
|
89
|
+
text: 'The gap that matters is between Tier 1 and Tier 2: the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API and `light-dark()` reached wide availability well before anchor positioning. So in Tier 2 browsers, layered surfaces open and dismiss correctly; they just are not positioned. This is the one feature most consumers will need to reason about.',
|
|
90
90
|
},
|
|
91
91
|
],
|
|
92
92
|
},
|
|
@@ -112,7 +112,7 @@ export const docs = {
|
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
type: 'prose',
|
|
115
|
-
text: 'If your product does not use any of these, it has no anchor-positioning requirement at all; it needs only `light-dark()` (Tier 2 and up) for correct theme colors.
|
|
115
|
+
text: 'If your product does not use any of these, it has no anchor-positioning requirement at all; it needs only `light-dark()` (Tier 2 and up) for correct theme colors. Page layout, typography, forms, buttons, cards, tables, and navigation all work down to Tier 2 with no special handling.',
|
|
116
116
|
},
|
|
117
117
|
],
|
|
118
118
|
},
|
|
@@ -123,7 +123,7 @@ export const docs = {
|
|
|
123
123
|
type: 'list',
|
|
124
124
|
style: 'do',
|
|
125
125
|
items: [
|
|
126
|
-
'Components never throw on missing platform APIs. Where a browser lacks the Popover API, layers fall back to plain visibility instead of crashing.',
|
|
126
|
+
'Components never throw on missing platform APIs. Where a browser lacks the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API, layers fall back to plain visibility instead of crashing.',
|
|
127
127
|
'Tier 1 and Tier 2 are officially supported and tested.',
|
|
128
128
|
'Non-layered components render correctly down to Tier 2.',
|
|
129
129
|
],
|
|
@@ -192,7 +192,7 @@ const hasLightDark = CSS.supports('color', 'light-dark(#000, #fff)');`,
|
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
type: 'prose',
|
|
195
|
-
text: 'This is not an arbitrary window: Baseline − 2 is close to where anchor positioning stops being available while the Popover API and `light-dark()` still are, so the tier boundary tracks a real capability edge, not a guessed date. The version floors above are reviewed and advanced roughly once a year as new Baseline years land. Always feature-detect rather than hardcoding version numbers, so your app adapts automatically as the platform moves.',
|
|
195
|
+
text: 'This is not an arbitrary window: Baseline − 2 is close to where anchor positioning stops being available while the [`Popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) API and `light-dark()` still are, so the tier boundary tracks a real capability edge, not a guessed date. The version floors above are reviewed and advanced roughly once a year as new Baseline years land. Always feature-detect rather than hardcoding version numbers, so your app adapts automatically as the platform moves.',
|
|
196
196
|
},
|
|
197
197
|
],
|
|
198
198
|
},
|
|
@@ -21,7 +21,7 @@ export const docs = {
|
|
|
21
21
|
type: 'code',
|
|
22
22
|
lang: 'text',
|
|
23
23
|
label: 'Paste this into your AI',
|
|
24
|
-
code: 'Install @astryxdesign/core, @astryxdesign/theme-neutral, and @astryxdesign/cli in this project, then run `npx @astryxdesign/cli init` to set up agent docs. Read the generated files to learn the conventions.',
|
|
24
|
+
code: 'Install @astryxdesign/core, @stylexjs/stylex, @astryxdesign/theme-neutral, and @astryxdesign/cli in this project, then run `npx @astryxdesign/cli init` to set up agent docs. Read the generated files to learn the conventions.',
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
27
|
},
|
|
@@ -34,13 +34,13 @@ export const docs = {
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
type: 'prose',
|
|
37
|
-
text: 'Add the core package, a theme
|
|
37
|
+
text: 'Add the core package and its `@stylexjs/stylex` peer dependency, plus a theme and the CLI.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'code',
|
|
41
41
|
lang: 'bash',
|
|
42
42
|
label: 'Terminal',
|
|
43
|
-
code: `npm install @astryxdesign/core @astryxdesign/theme-neutral @astryxdesign/cli`,
|
|
43
|
+
code: `npm install @astryxdesign/core @stylexjs/stylex @astryxdesign/theme-neutral @astryxdesign/cli`,
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
type: 'prose',
|
|
@@ -114,9 +114,9 @@ export const brandTheme = defineTheme({
|
|
|
114
114
|
type: 'list',
|
|
115
115
|
style: 'ordered',
|
|
116
116
|
items: [
|
|
117
|
-
'Add the name to IconName type in packages/core/src/Icon/globalIconRegistry.tsx',
|
|
118
|
-
'Add the default SVG to packages/core/src/Icon/defaultIcons.tsx',
|
|
119
|
-
'Add a row to the Available Names table in packages/cli/docs/icons.doc.mjs',
|
|
117
|
+
'Add the name to IconName type in `packages/core/src/Icon/globalIconRegistry.tsx`',
|
|
118
|
+
'Add the default SVG to `packages/core/src/Icon/defaultIcons.tsx`',
|
|
119
|
+
'Add a row to the Available Names table in `packages/cli/assets/docs/icons.doc.mjs`',
|
|
120
120
|
],
|
|
121
121
|
},
|
|
122
122
|
],
|