@staffbase/design 19.0.0 → 19.0.1
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.
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# v17 — Migrate Tailwind tokens
|
|
2
|
+
|
|
3
|
+
This codemod automatically migrates renamed Tailwind tokens introduced as breaking changes in `@staffbase/design` v17.
|
|
4
|
+
|
|
5
|
+
## What it transforms
|
|
6
|
+
|
|
7
|
+
### Border radius
|
|
8
|
+
|
|
9
|
+
All Tailwind positional prefixes are handled (`t`, `b`, `l`, `r`, `tl`, `tr`, `bl`, `br`, `s`, `e`, `ss`, `se`, `es`, `ee`):
|
|
10
|
+
|
|
11
|
+
| Before | After |
|
|
12
|
+
| ------------------- | -------------------- |
|
|
13
|
+
| `rounded-4` | `rounded-sm` |
|
|
14
|
+
| `rounded-6` | `rounded-md` |
|
|
15
|
+
| `rounded-8` | `rounded-lg` |
|
|
16
|
+
| `rounded-16` | `rounded-xl` |
|
|
17
|
+
| `rounded-t-4` | `rounded-t-sm` |
|
|
18
|
+
| `rounded-e-8` | `rounded-e-lg` |
|
|
19
|
+
| `rounded-tl-16` | `rounded-tl-xl` |
|
|
20
|
+
| `sm:rounded-4` | `sm:rounded-sm` |
|
|
21
|
+
| `hover:rounded-t-6` | `hover:rounded-t-md` |
|
|
22
|
+
|
|
23
|
+
### Font size
|
|
24
|
+
|
|
25
|
+
| Before | After |
|
|
26
|
+
| --------- | ---------- |
|
|
27
|
+
| `text-12` | `text-xs` |
|
|
28
|
+
| `text-14` | `text-sm` |
|
|
29
|
+
| `text-16` | `text-md` |
|
|
30
|
+
| `text-18` | `text-lg` |
|
|
31
|
+
| `text-20` | `text-xl` |
|
|
32
|
+
| `text-24` | `text-2xl` |
|
|
33
|
+
| `text-28` | `text-3xl` |
|
|
34
|
+
| `text-32` | `text-4xl` |
|
|
35
|
+
| `text-36` | `text-5xl` |
|
|
36
|
+
|
|
37
|
+
### Font weight
|
|
38
|
+
|
|
39
|
+
| Before | After |
|
|
40
|
+
| -------------- | ------------- |
|
|
41
|
+
| `font-regular` | `font-normal` |
|
|
42
|
+
|
|
43
|
+
### Letter spacing
|
|
44
|
+
|
|
45
|
+
| Before | After |
|
|
46
|
+
| ------------------ | ----------------- |
|
|
47
|
+
| `tracking-default` | `tracking-normal` |
|
|
48
|
+
|
|
49
|
+
### Pill
|
|
50
|
+
|
|
51
|
+
The `variant` prop (previously used for colour) is replaced by `color`. Only static string values are transformed; dynamic expressions will surface as TypeScript errors after migration.
|
|
52
|
+
|
|
53
|
+
| Before | After |
|
|
54
|
+
| ------------------------- | ------------------------- |
|
|
55
|
+
| `<Pill variant="blue">` | `<Pill color="primary">` |
|
|
56
|
+
| `<Pill variant="green">` | `<Pill color="success">` |
|
|
57
|
+
| `<Pill variant="grey">` | `<Pill color="neutral">` |
|
|
58
|
+
| `<Pill variant="red">` | `<Pill color="critical">` |
|
|
59
|
+
| `<Pill variant="yellow">` | `<Pill color="warning">` |
|
|
60
|
+
|
|
61
|
+
`variant="outline"` and `variant="solid"` are left untouched — they already use the new API.
|
|
62
|
+
|
|
63
|
+
### PillGroup
|
|
64
|
+
|
|
65
|
+
`PillGroup` is removed. Its usage is replaced with a plain `div`:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// Before
|
|
69
|
+
import {PillGroup} from '@staffbase/design';
|
|
70
|
+
<PillGroup><Pill>One</Pill><Pill>Two</Pill></PillGroup>
|
|
71
|
+
|
|
72
|
+
// After
|
|
73
|
+
<div className='flex flex-wrap gap-1'><Pill>One</Pill><Pill>Two</Pill></div>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A static `className` prop is merged into the base classes:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// Before
|
|
80
|
+
<PillGroup className='mt-4'>…</PillGroup>
|
|
81
|
+
|
|
82
|
+
// After
|
|
83
|
+
<div className='flex flex-wrap gap-1 mt-4'>…</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `PillGroup` import is removed automatically. Other props (`id`, `data-*`, `aria-*`, etc.) are preserved on the resulting `div`.
|
|
87
|
+
|
|
88
|
+
### TextArea
|
|
89
|
+
|
|
90
|
+
`TextArea` is renamed to `TextAreaDeprecated` and `TextAreaProps` to `TextAreaDeprecatedProps`. Import specifiers, JSX usages, and TypeScript type references are all updated.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Before
|
|
94
|
+
import {TextArea, TextAreaProps} from '@staffbase/design';
|
|
95
|
+
const props: TextAreaProps = {};
|
|
96
|
+
<TextArea {...props} />;
|
|
97
|
+
|
|
98
|
+
// After
|
|
99
|
+
import {TextAreaDeprecated, TextAreaDeprecatedProps} from '@staffbase/design';
|
|
100
|
+
const props: TextAreaDeprecatedProps = {};
|
|
101
|
+
<TextAreaDeprecated {...props} />;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Aliased imports are handled correctly — only the exported name changes, the local alias is preserved:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// Before
|
|
108
|
+
import {TextArea as TA} from '@staffbase/design';
|
|
109
|
+
|
|
110
|
+
// After
|
|
111
|
+
import {TextAreaDeprecated as TA} from '@staffbase/design';
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Tokens are replaced in all string contexts** — `className` props, `clsx()`/`cn()` calls, variant maps, and constants:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// Before
|
|
118
|
+
<div className="rounded-4 text-14 font-regular" />
|
|
119
|
+
clsx('rounded-8', isActive && 'rounded-t-16')
|
|
120
|
+
const styles = {base: 'tracking-default text-12 rounded-s-6'}
|
|
121
|
+
|
|
122
|
+
// After
|
|
123
|
+
<div className="rounded-sm text-sm font-normal" />
|
|
124
|
+
clsx('rounded-lg', isActive && 'rounded-t-xl')
|
|
125
|
+
const styles = {base: 'tracking-normal text-xs rounded-s-md'}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
### Step 1 — Install jscodeshift
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm install --save-dev jscodeshift
|
|
134
|
+
# or
|
|
135
|
+
pnpm add -D jscodeshift
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Step 2 — Run the codemod
|
|
139
|
+
|
|
140
|
+
Point the `--transform` flag at this file inside your `node_modules`:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx jscodeshift@latest \
|
|
144
|
+
--transform node_modules/@staffbase/design/codemods/v17-migrate-tokens/transform.js \
|
|
145
|
+
--extensions tsx,ts,jsx,js \
|
|
146
|
+
--parser tsx \
|
|
147
|
+
src/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Run a **dry run** first to preview changes without writing files:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx jscodeshift@latest \
|
|
154
|
+
--transform node_modules/@staffbase/design/codemods/v17-migrate-tokens/transform.js \
|
|
155
|
+
--extensions tsx,ts,jsx,js \
|
|
156
|
+
--parser tsx \
|
|
157
|
+
--dry \
|
|
158
|
+
--print \
|
|
159
|
+
src/
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Step 3 — Review and commit
|
|
163
|
+
|
|
164
|
+
1. Run your type-checker:
|
|
165
|
+
```bash
|
|
166
|
+
npx tsc --noEmit
|
|
167
|
+
```
|
|
168
|
+
2. Run your tests.
|
|
169
|
+
3. Commit the result.
|
|
170
|
+
|
|
171
|
+
## What the codemod does NOT handle
|
|
172
|
+
|
|
173
|
+
- **CSS files** — `@apply rounded-4` in `.css` files is not transformed. Update these manually.
|
|
174
|
+
- **Dynamic class composition** — e.g. `` `rounded-${size}` `` where the suffix is a variable. Only static string content is replaced.
|
|
175
|
+
- **Dynamic `className` on `PillGroup`** — e.g. `<PillGroup className={cn('mt-4')}>`. The dynamic value is replaced with the base classes only; merge manually afterward.
|
|
176
|
+
- **Spacing tokens** — the v17 spacing scale change (divide by 4 for margins, padding, gap, etc.) is a numeric transformation that cannot be automated safely. Refer to the v17 migration guide.
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jscodeshift codemod: Migrate v17 Tailwind token renames.
|
|
3
|
+
*
|
|
4
|
+
* Replaces renamed tokens in all string literals (including clsx/cn calls and
|
|
5
|
+
* template literals) across JS/TS/JSX/TSX files.
|
|
6
|
+
*
|
|
7
|
+
* Border radius — all Tailwind positional prefixes handled
|
|
8
|
+
* (t, b, l, r, tl, tr, bl, br, s, e, ss, se, es, ee):
|
|
9
|
+
* rounded-{prefix?}-4 → rounded-{prefix?}-sm
|
|
10
|
+
* rounded-{prefix?}-6 → rounded-{prefix?}-md
|
|
11
|
+
* rounded-{prefix?}-8 → rounded-{prefix?}-lg
|
|
12
|
+
* rounded-{prefix?}-16 → rounded-{prefix?}-xl
|
|
13
|
+
*
|
|
14
|
+
* Font size:
|
|
15
|
+
* text-12 → text-xs text-24 → text-2xl
|
|
16
|
+
* text-14 → text-sm text-28 → text-3xl
|
|
17
|
+
* text-16 → text-md text-32 → text-4xl
|
|
18
|
+
* text-18 → text-lg text-36 → text-5xl
|
|
19
|
+
* text-20 → text-xl
|
|
20
|
+
*
|
|
21
|
+
* Font weight:
|
|
22
|
+
* font-regular → font-normal
|
|
23
|
+
*
|
|
24
|
+
* Letter spacing:
|
|
25
|
+
* tracking-default → tracking-normal
|
|
26
|
+
*
|
|
27
|
+
* Breakpoints (only as responsive prefixes — e.g. 3xl:flex, not text-3xl):
|
|
28
|
+
* 3xl → 2xl
|
|
29
|
+
* 2xs → xs
|
|
30
|
+
*
|
|
31
|
+
* Background overlay:
|
|
32
|
+
* bg-overlay-default → bg-neutral-strong/50
|
|
33
|
+
* bg-overlay-strong → bg-neutral-strong/80
|
|
34
|
+
*
|
|
35
|
+
* Pill — JSX prop migration (scoped to <Pill> imported from @staffbase/design):
|
|
36
|
+
* variant="blue" → color="primary"
|
|
37
|
+
* variant="green" → color="success"
|
|
38
|
+
* variant="grey" → color="neutral"
|
|
39
|
+
* variant="red" → color="critical"
|
|
40
|
+
* variant="yellow" → color="warning"
|
|
41
|
+
*
|
|
42
|
+
* PillGroup — replaced with a plain div:
|
|
43
|
+
* <PillGroup> → <div className="flex flex-wrap gap-1">
|
|
44
|
+
*
|
|
45
|
+
* TextArea — renamed to the deprecated export:
|
|
46
|
+
* TextArea → TextAreaDeprecated
|
|
47
|
+
* TextAreaProps → TextAreaDeprecatedProps
|
|
48
|
+
*
|
|
49
|
+
* Tooltip — swapped with the new implementation:
|
|
50
|
+
* Tooltip → TooltipDeprecated
|
|
51
|
+
* TooltipNew → Tooltip
|
|
52
|
+
*
|
|
53
|
+
* PillProps — replaced with an inlined type query:
|
|
54
|
+
* PillProps → React.ComponentProps<typeof Pill>
|
|
55
|
+
*
|
|
56
|
+
* Usage:
|
|
57
|
+
* npx jscodeshift@latest \
|
|
58
|
+
* --transform node_modules/@staffbase/design/codemods/v17-migrate-tokens/transform.js \
|
|
59
|
+
* --extensions tsx,ts,jsx,js \
|
|
60
|
+
* --parser tsx \
|
|
61
|
+
* src/
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
// ── Token mappings ────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const ROUNDED_SIZES = {4: 'sm', 6: 'md', 8: 'lg', 16: 'xl'};
|
|
67
|
+
|
|
68
|
+
// All Tailwind border-radius positional prefixes. Empty string = no prefix (e.g. rounded-4).
|
|
69
|
+
const ROUNDED_PREFIXES = [
|
|
70
|
+
'',
|
|
71
|
+
't-',
|
|
72
|
+
'b-',
|
|
73
|
+
'l-',
|
|
74
|
+
'r-',
|
|
75
|
+
'tl-',
|
|
76
|
+
'tr-',
|
|
77
|
+
'bl-',
|
|
78
|
+
'br-',
|
|
79
|
+
's-',
|
|
80
|
+
'e-',
|
|
81
|
+
'ss-',
|
|
82
|
+
'se-',
|
|
83
|
+
'es-',
|
|
84
|
+
'ee-',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const TEXT_SIZES = {
|
|
88
|
+
12: 'xs',
|
|
89
|
+
14: 'sm',
|
|
90
|
+
16: 'md',
|
|
91
|
+
18: 'lg',
|
|
92
|
+
20: 'xl',
|
|
93
|
+
24: '2xl',
|
|
94
|
+
28: '3xl',
|
|
95
|
+
32: '4xl',
|
|
96
|
+
36: '5xl',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DIRECT_RENAMES = [
|
|
100
|
+
['font-regular', 'font-normal'],
|
|
101
|
+
['tracking-default', 'tracking-normal'],
|
|
102
|
+
['bg-overlay-default', 'bg-neutral-strong/50'],
|
|
103
|
+
['bg-overlay-strong', 'bg-neutral-strong/80'],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Breakpoint prefixes only — matched with a lookahead for `:` to avoid touching
|
|
107
|
+
// size suffixes like `text-3xl` or `rounded-2xl`.
|
|
108
|
+
const BREAKPOINT_RENAMES = [
|
|
109
|
+
['3xl', '2xl'],
|
|
110
|
+
['2xs', 'xs'],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const DESIGN_PKG = '@staffbase/design';
|
|
114
|
+
|
|
115
|
+
const PILL_GROUP_CLASSES = 'flex flex-wrap gap-1';
|
|
116
|
+
|
|
117
|
+
const TEXT_AREA_RENAMES = new Map([
|
|
118
|
+
['TextArea', 'TextAreaDeprecated'],
|
|
119
|
+
['TextAreaProps', 'TextAreaDeprecatedProps'],
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const TOOLTIP_RENAMES = new Map([
|
|
123
|
+
['Tooltip', 'TooltipDeprecated'],
|
|
124
|
+
['TooltipNew', 'Tooltip'],
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
// Pill: old variant color values → new color prop values.
|
|
128
|
+
// variant="outline" | "solid" are left untouched (they already use the new API).
|
|
129
|
+
const PILL_COLOR_MAP = {
|
|
130
|
+
blue: 'primary',
|
|
131
|
+
green: 'success',
|
|
132
|
+
grey: 'neutral',
|
|
133
|
+
red: 'critical',
|
|
134
|
+
yellow: 'warning',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ── Pre-build replacement list ────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** @type {Array<[RegExp, string]>} */
|
|
140
|
+
const REPLACEMENTS = [];
|
|
141
|
+
|
|
142
|
+
for (const [size, name] of Object.entries(ROUNDED_SIZES)) {
|
|
143
|
+
for (const prefix of ROUNDED_PREFIXES) {
|
|
144
|
+
REPLACEMENTS.push([new RegExp(`\\brounded-${prefix}${size}\\b`, 'g'), `rounded-${prefix}${name}`]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const [size, name] of Object.entries(TEXT_SIZES)) {
|
|
149
|
+
REPLACEMENTS.push([new RegExp(`\\btext-${size}\\b`, 'g'), `text-${name}`]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const [from, to] of DIRECT_RENAMES) {
|
|
153
|
+
REPLACEMENTS.push([new RegExp(`\\b${from}\\b`, 'g'), to]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Breakpoints: match only when used as a responsive prefix (followed by `:`),
|
|
157
|
+
// not when part of a size utility (e.g. text-3xl, rounded-2xl).
|
|
158
|
+
for (const [from, to] of BREAKPOINT_RENAMES) {
|
|
159
|
+
REPLACEMENTS.push([new RegExp(`\\b${from}(?=:)`, 'g'), to]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function replaceTokens(str) {
|
|
165
|
+
let result = str;
|
|
166
|
+
for (const [pattern, replacement] of REPLACEMENTS) {
|
|
167
|
+
result = result.replace(pattern, replacement);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Pill migration ────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function migratePills(j, root) {
|
|
175
|
+
const designImports = root.find(j.ImportDeclaration, {source: {value: DESIGN_PKG}});
|
|
176
|
+
if (!designImports.length) return false;
|
|
177
|
+
|
|
178
|
+
let pillLocalName = null;
|
|
179
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
180
|
+
if (spec.node.imported.name === 'Pill') {
|
|
181
|
+
pillLocalName = spec.node.local ? spec.node.local.name : 'Pill';
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
if (!pillLocalName) return false;
|
|
185
|
+
|
|
186
|
+
let dirty = false;
|
|
187
|
+
root.find(j.JSXElement, {openingElement: {name: {name: pillLocalName}}}).forEach((path) => {
|
|
188
|
+
const attrs = path.node.openingElement.attributes;
|
|
189
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
190
|
+
const attr = attrs[i];
|
|
191
|
+
if (attr.type !== 'JSXAttribute' || attr.name.name !== 'variant') continue;
|
|
192
|
+
if (!attr.value || attr.value.type !== 'StringLiteral') continue;
|
|
193
|
+
|
|
194
|
+
const newColor = PILL_COLOR_MAP[attr.value.value];
|
|
195
|
+
if (!newColor) continue; // variant="outline" | "solid" — already new API, skip
|
|
196
|
+
|
|
197
|
+
attrs[i] = j.jsxAttribute(j.jsxIdentifier('color'), j.stringLiteral(newColor));
|
|
198
|
+
dirty = true;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return dirty;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── PillGroup migration ───────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function migratePillGroups(j, root) {
|
|
208
|
+
const designImports = root.find(j.ImportDeclaration, {source: {value: DESIGN_PKG}});
|
|
209
|
+
if (!designImports.length) return false;
|
|
210
|
+
|
|
211
|
+
let localName = null;
|
|
212
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
213
|
+
if (spec.node.imported.name === 'PillGroup') {
|
|
214
|
+
localName = spec.node.local ? spec.node.local.name : 'PillGroup';
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
if (!localName) return false;
|
|
218
|
+
|
|
219
|
+
let dirty = false;
|
|
220
|
+
root.find(j.JSXElement, {openingElement: {name: {name: localName}}}).forEach((path) => {
|
|
221
|
+
const {openingElement, closingElement} = path.node;
|
|
222
|
+
|
|
223
|
+
// Merge base classes with any existing static className.
|
|
224
|
+
const existingClassAttr = openingElement.attributes.find(
|
|
225
|
+
(attr) => attr.type === 'JSXAttribute' && attr.name.name === 'className',
|
|
226
|
+
);
|
|
227
|
+
let mergedClass = PILL_GROUP_CLASSES;
|
|
228
|
+
if (existingClassAttr?.value?.type === 'StringLiteral') {
|
|
229
|
+
mergedClass = `${PILL_GROUP_CLASSES} ${existingClassAttr.value.value}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Build new attrs: className first, then all other existing attrs (minus old className).
|
|
233
|
+
const otherAttrs = openingElement.attributes.filter(
|
|
234
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name.name === 'className'),
|
|
235
|
+
);
|
|
236
|
+
openingElement.name = j.jsxIdentifier('div');
|
|
237
|
+
openingElement.attributes = [
|
|
238
|
+
j.jsxAttribute(j.jsxIdentifier('className'), j.stringLiteral(mergedClass)),
|
|
239
|
+
...otherAttrs,
|
|
240
|
+
];
|
|
241
|
+
if (closingElement) closingElement.name = j.jsxIdentifier('div');
|
|
242
|
+
|
|
243
|
+
dirty = true;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!dirty) return false;
|
|
247
|
+
|
|
248
|
+
// Remove PillGroup import specifier.
|
|
249
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
250
|
+
if (spec.node.imported.name === 'PillGroup') j(spec).remove();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── TextArea migration ───────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
function migrateTextArea(j, root) {
|
|
259
|
+
const designImports = root.find(j.ImportDeclaration, {source: {value: DESIGN_PKG}});
|
|
260
|
+
if (!designImports.length) return false;
|
|
261
|
+
|
|
262
|
+
// Collect local → new name mappings for non-aliased imports.
|
|
263
|
+
const localRenames = new Map();
|
|
264
|
+
|
|
265
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
266
|
+
const importedName = spec.node.imported.name;
|
|
267
|
+
const newImportedName = TEXT_AREA_RENAMES.get(importedName);
|
|
268
|
+
if (!newImportedName) return;
|
|
269
|
+
|
|
270
|
+
const localName = spec.node.local.name;
|
|
271
|
+
spec.node.imported = j.identifier(newImportedName);
|
|
272
|
+
|
|
273
|
+
// When no alias, local name equals imported name — rename local usages too.
|
|
274
|
+
if (localName === importedName) {
|
|
275
|
+
spec.node.local = j.identifier(newImportedName);
|
|
276
|
+
localRenames.set(localName, newImportedName);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (!localRenames.size) return false;
|
|
281
|
+
|
|
282
|
+
localRenames.forEach((newName, oldName) => {
|
|
283
|
+
root.find(j.Identifier, {name: oldName}).forEach((path) => {
|
|
284
|
+
const parent = path.parent.node;
|
|
285
|
+
if (parent.type === 'ImportSpecifier') return; // already handled above
|
|
286
|
+
// Skip property accesses (foo.TextArea) and non-computed object keys.
|
|
287
|
+
if (parent.type === 'MemberExpression' && parent.property === path.node && !parent.computed) return;
|
|
288
|
+
if (parent.type === 'ObjectProperty' && parent.key === path.node && !parent.computed) return;
|
|
289
|
+
path.node.name = newName;
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Tooltip migration ────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
function migrateTooltip(j, root) {
|
|
299
|
+
const designImports = root.find(j.ImportDeclaration, {source: {value: DESIGN_PKG}});
|
|
300
|
+
if (!designImports.length) return false;
|
|
301
|
+
|
|
302
|
+
// Collect all renames first, then apply — avoids conflicts when both
|
|
303
|
+
// Tooltip and TooltipNew are imported in the same file.
|
|
304
|
+
const localRenames = new Map();
|
|
305
|
+
|
|
306
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
307
|
+
const importedName = spec.node.imported.name;
|
|
308
|
+
const newImportedName = TOOLTIP_RENAMES.get(importedName);
|
|
309
|
+
if (!newImportedName) return;
|
|
310
|
+
|
|
311
|
+
const localName = spec.node.local.name;
|
|
312
|
+
spec.node.imported = j.identifier(newImportedName);
|
|
313
|
+
|
|
314
|
+
if (localName === importedName) {
|
|
315
|
+
spec.node.local = j.identifier(newImportedName);
|
|
316
|
+
localRenames.set(localName, newImportedName);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!localRenames.size) return false;
|
|
321
|
+
|
|
322
|
+
localRenames.forEach((newName, oldName) => {
|
|
323
|
+
root.find(j.Identifier, {name: oldName}).forEach((path) => {
|
|
324
|
+
const parent = path.parent.node;
|
|
325
|
+
if (parent.type === 'ImportSpecifier') return;
|
|
326
|
+
if (parent.type === 'MemberExpression' && parent.property === path.node && !parent.computed) return;
|
|
327
|
+
if (parent.type === 'ObjectProperty' && parent.key === path.node && !parent.computed) return;
|
|
328
|
+
path.node.name = newName;
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── PillProps migration ───────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
function migratePillProps(j, root) {
|
|
338
|
+
const designImports = root.find(j.ImportDeclaration, {source: {value: DESIGN_PKG}});
|
|
339
|
+
if (!designImports.length) return false;
|
|
340
|
+
|
|
341
|
+
let pillPropsLocalName = null;
|
|
342
|
+
let pillLocalName = null;
|
|
343
|
+
|
|
344
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
345
|
+
if (spec.node.imported.name === 'PillProps') pillPropsLocalName = spec.node.local.name;
|
|
346
|
+
if (spec.node.imported.name === 'Pill') pillLocalName = spec.node.local.name;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (!pillPropsLocalName) return false;
|
|
350
|
+
|
|
351
|
+
// `typeof Pill` requires Pill to be imported as a value. Add it if missing.
|
|
352
|
+
if (!pillLocalName) {
|
|
353
|
+
pillLocalName = 'Pill';
|
|
354
|
+
designImports.get().node.specifiers.push(j.importSpecifier(j.identifier('Pill')));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const buildReplacementType = (localPillName) =>
|
|
358
|
+
j.tsTypeReference(
|
|
359
|
+
j.tsQualifiedName(j.identifier('React'), j.identifier('ComponentProps')),
|
|
360
|
+
j.tsTypeParameterInstantiation([j.tsTypeQuery(j.identifier(localPillName))]),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Replace all TSTypeReference nodes that use the old local name.
|
|
364
|
+
let dirty = false;
|
|
365
|
+
root.find(j.TSTypeReference).forEach((path) => {
|
|
366
|
+
const {typeName} = path.node;
|
|
367
|
+
if (typeName.type === 'Identifier' && typeName.name === pillPropsLocalName) {
|
|
368
|
+
j(path).replaceWith(buildReplacementType(pillLocalName));
|
|
369
|
+
dirty = true;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!dirty) return false;
|
|
374
|
+
|
|
375
|
+
// Remove the PillProps import specifier.
|
|
376
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
377
|
+
if (spec.node.imported.name === 'PillProps') j(spec).remove();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
export default function transform(file, api) {
|
|
386
|
+
const j = api.jscodeshift;
|
|
387
|
+
const root = j(file.source);
|
|
388
|
+
let dirty = false;
|
|
389
|
+
|
|
390
|
+
// Replace within all string literals (className props, clsx/cn args, constants, etc.).
|
|
391
|
+
root.find(j.StringLiteral).forEach((path) => {
|
|
392
|
+
const next = replaceTokens(path.node.value);
|
|
393
|
+
if (next !== path.node.value) {
|
|
394
|
+
path.node.value = next;
|
|
395
|
+
dirty = true;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Replace within the static parts of template literals.
|
|
400
|
+
root.find(j.TemplateElement).forEach((path) => {
|
|
401
|
+
const {raw, cooked} = path.node.value;
|
|
402
|
+
const nextRaw = replaceTokens(raw);
|
|
403
|
+
if (nextRaw !== raw) {
|
|
404
|
+
path.node.value = {
|
|
405
|
+
raw: nextRaw,
|
|
406
|
+
cooked: cooked != null ? replaceTokens(cooked) : cooked,
|
|
407
|
+
};
|
|
408
|
+
dirty = true;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Migrate <Pill variant="color"> → <Pill color="newColor">.
|
|
413
|
+
if (migratePills(j, root)) dirty = true;
|
|
414
|
+
|
|
415
|
+
// Migrate <PillGroup> → <div className="flex flex-wrap gap-1">.
|
|
416
|
+
if (migratePillGroups(j, root)) dirty = true;
|
|
417
|
+
|
|
418
|
+
// Rename TextArea → TextAreaDeprecated and TextAreaProps → TextAreaDeprecatedProps.
|
|
419
|
+
if (migrateTextArea(j, root)) dirty = true;
|
|
420
|
+
|
|
421
|
+
// Rename Tooltip → TooltipDeprecated and TooltipNew → Tooltip.
|
|
422
|
+
if (migrateTooltip(j, root)) dirty = true;
|
|
423
|
+
|
|
424
|
+
// Replace PillProps with React.ComponentProps<typeof Pill>.
|
|
425
|
+
if (migratePillProps(j, root)) dirty = true;
|
|
426
|
+
|
|
427
|
+
return dirty ? root.toSource({quote: 'single'}) : undefined;
|
|
428
|
+
}
|