@atlaskit/eslint-plugin-design-system 13.30.0 → 13.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +75 -75
- package/dist/cjs/presets/all-flat.codegen.js +2 -2
- package/dist/cjs/presets/all.codegen.js +2 -2
- package/dist/cjs/rules/index.codegen.js +3 -3
- package/dist/cjs/rules/lozenge-badge-tag-labelling-system-migration/index.js +712 -0
- package/dist/es2019/presets/all-flat.codegen.js +2 -2
- package/dist/es2019/presets/all.codegen.js +2 -2
- package/dist/es2019/rules/index.codegen.js +3 -3
- package/dist/es2019/rules/lozenge-badge-tag-labelling-system-migration/index.js +702 -0
- package/dist/esm/presets/all-flat.codegen.js +2 -2
- package/dist/esm/presets/all.codegen.js +2 -2
- package/dist/esm/rules/index.codegen.js +3 -3
- package/dist/esm/rules/lozenge-badge-tag-labelling-system-migration/index.js +705 -0
- package/dist/types/presets/all-flat.codegen.d.ts +1 -1
- package/dist/types/presets/all.codegen.d.ts +1 -1
- package/dist/types/rules/index.codegen.d.ts +1 -1
- package/dist/types-ts4.5/presets/all-flat.codegen.d.ts +1 -1
- package/dist/types-ts4.5/presets/all.codegen.d.ts +1 -1
- package/dist/types-ts4.5/rules/index.codegen.d.ts +1 -1
- package/package.json +2 -2
- package/dist/cjs/rules/lozenge-isBold-and-lozenge-badge-appearance-migration/index.js +0 -332
- package/dist/es2019/rules/lozenge-isBold-and-lozenge-badge-appearance-migration/index.js +0 -324
- package/dist/esm/rules/lozenge-isBold-and-lozenge-badge-appearance-migration/index.js +0 -326
- /package/dist/types/rules/{lozenge-isBold-and-lozenge-badge-appearance-migration → lozenge-badge-tag-labelling-system-migration}/index.d.ts +0 -0
- /package/dist/types-ts4.5/rules/{lozenge-isBold-and-lozenge-badge-appearance-migration → lozenge-badge-tag-labelling-system-migration}/index.d.ts +0 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { isNodeOfType } from 'eslint-codemod-utils';
|
|
2
|
+
import { createLintRule } from '../utils/create-rule';
|
|
3
|
+
const rule = createLintRule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: 'lozenge-badge-tag-labelling-system-migration',
|
|
6
|
+
fixable: 'code',
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Helps migrate Lozenge isBold prop, Badge appearance values, and SimpleTag/RemovableTag components as part of the Labelling System Phase 1 migration.',
|
|
10
|
+
recommended: false,
|
|
11
|
+
severity: 'warn'
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
updateAppearance: 'Update appearance value to new semantic value.',
|
|
15
|
+
migrateTag: 'Non-bold <Lozenge> variants should migrate to <Tag> component.',
|
|
16
|
+
manualReview: "Dynamic 'isBold' props require manual review before migration.",
|
|
17
|
+
dynamicLozengeAppearance: "Dynamic 'appearance' prop values require manual review before migrating to Tag. Please verify the appearance value and manually convert it to the appropriate color prop value.",
|
|
18
|
+
updateBadgeAppearance: 'Update Badge appearance value "{{oldValue}}" to new semantic value "{{newValue}}".',
|
|
19
|
+
dynamicBadgeAppearance: 'Dynamic appearance prop values require manual review to ensure they use the new semantic values: neutral, information, inverse, danger, success.'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
/**
|
|
24
|
+
* Contains a map of imported Lozenge components.
|
|
25
|
+
*/
|
|
26
|
+
const lozengeImports = {}; // local name -> import source
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Contains a map of imported Badge components.
|
|
30
|
+
*/
|
|
31
|
+
const badgeImports = {}; // local name -> import source
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Contains a map of imported Tag components (SimpleTag, RemovableTag, or default Tag imports).
|
|
35
|
+
* Maps local name to { type: 'SimpleTag' | 'RemovableTag' | 'Tag', source: string, node: ImportNode }
|
|
36
|
+
*/
|
|
37
|
+
const tagImports = {};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tracks which tag imports need to migrate to Tag (default) or AvatarTag (named)
|
|
41
|
+
* Maps local name to migration target: 'Tag' | 'AvatarTag'
|
|
42
|
+
*/
|
|
43
|
+
const tagMigrationTargets = {};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Tracks import declaration nodes that need to be updated
|
|
47
|
+
*/
|
|
48
|
+
const importDeclarationsToUpdate = new Set();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Contains a map of imported Avatar components from @atlaskit/avatar.
|
|
52
|
+
* Maps local name to import source
|
|
53
|
+
*/
|
|
54
|
+
const avatarImports = {};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Contains a map of imported Tag and AvatarTag components from @atlaskit/tag.
|
|
58
|
+
* These are the new components that should not be migrated.
|
|
59
|
+
* Maps local name to import source
|
|
60
|
+
*/
|
|
61
|
+
const newTagImports = {};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if a JSX attribute value is a literal false
|
|
65
|
+
*/
|
|
66
|
+
function isLiteralFalse(node) {
|
|
67
|
+
return node && node.type === 'JSXExpressionContainer' && node.expression && node.expression.type === 'Literal' && node.expression.value === false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a JSX attribute value is dynamic (not a static literal value)
|
|
72
|
+
* Can be used for any prop type (boolean, string, etc.)
|
|
73
|
+
*/
|
|
74
|
+
function isDynamicExpression(node) {
|
|
75
|
+
if (!node) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If it's a plain literal (e.g., appearance="value"), it's not dynamic
|
|
80
|
+
if (node.type === 'Literal') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If it's an expression container with a non-literal expression, it's dynamic
|
|
85
|
+
if (node.type === 'JSXExpressionContainer') {
|
|
86
|
+
const expr = node.expression;
|
|
87
|
+
return expr && expr.type !== 'Literal';
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all attributes as an object for easier manipulation
|
|
94
|
+
*/
|
|
95
|
+
function getAttributesMap(attributes) {
|
|
96
|
+
const map = {};
|
|
97
|
+
attributes.forEach(attr => {
|
|
98
|
+
if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
|
|
99
|
+
map[attr.name.name] = attr;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return map;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Map old appearance values to new semantic appearance values
|
|
107
|
+
* Both Lozenge and Tag now use the same appearance prop with new semantic values
|
|
108
|
+
*/
|
|
109
|
+
function mapToNewAppearanceValue(oldValue) {
|
|
110
|
+
const mapping = {
|
|
111
|
+
success: 'success',
|
|
112
|
+
default: 'default',
|
|
113
|
+
removed: 'removed',
|
|
114
|
+
inprogress: 'inprogress',
|
|
115
|
+
new: 'new',
|
|
116
|
+
moved: 'moved'
|
|
117
|
+
};
|
|
118
|
+
return mapping[oldValue] || oldValue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Map Lozenge appearance values to Tag color values
|
|
123
|
+
* Used when migrating Lozenge to Tag component
|
|
124
|
+
*/
|
|
125
|
+
function mapLozengeAppearanceToTagColor(appearanceValue) {
|
|
126
|
+
const mapping = {
|
|
127
|
+
success: 'lime',
|
|
128
|
+
default: 'gray',
|
|
129
|
+
removed: 'red',
|
|
130
|
+
inprogress: 'blue',
|
|
131
|
+
new: 'purple',
|
|
132
|
+
moved: 'yellow'
|
|
133
|
+
};
|
|
134
|
+
return mapping[appearanceValue] || appearanceValue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Map Badge old appearance values to new semantic appearance values
|
|
139
|
+
*/
|
|
140
|
+
function mapBadgeToNewAppearanceValue(oldValue) {
|
|
141
|
+
const mapping = {
|
|
142
|
+
added: 'success',
|
|
143
|
+
removed: 'danger',
|
|
144
|
+
default: 'neutral',
|
|
145
|
+
primary: 'information',
|
|
146
|
+
primaryInverted: 'inverse',
|
|
147
|
+
important: 'danger'
|
|
148
|
+
};
|
|
149
|
+
return mapping[oldValue] || oldValue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Map Tag color light variants to semantic color values
|
|
154
|
+
*/
|
|
155
|
+
function mapTagColorValue(oldValue) {
|
|
156
|
+
const mapping = {
|
|
157
|
+
limeLight: 'lime',
|
|
158
|
+
orangeLight: 'orange',
|
|
159
|
+
magentaLight: 'magenta',
|
|
160
|
+
greenLight: 'green',
|
|
161
|
+
blueLight: 'blue',
|
|
162
|
+
redLight: 'red',
|
|
163
|
+
purpleLight: 'purple',
|
|
164
|
+
greyLight: 'gray',
|
|
165
|
+
tealLight: 'teal',
|
|
166
|
+
yellowLight: 'yellow',
|
|
167
|
+
grey: 'gray'
|
|
168
|
+
};
|
|
169
|
+
return mapping[oldValue] || oldValue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if elemBefore prop contains only an Avatar component from @atlaskit/avatar
|
|
174
|
+
* Returns the Avatar component name if it's from the avatar package, null otherwise
|
|
175
|
+
*/
|
|
176
|
+
function getAvatarComponentName(elemBeforeProp) {
|
|
177
|
+
if (!elemBeforeProp || !elemBeforeProp.value) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const value = elemBeforeProp.value;
|
|
181
|
+
|
|
182
|
+
// Check for JSX element: <Avatar ... />
|
|
183
|
+
if (value.type === 'JSXElement' && value.openingElement.name.name === 'Avatar') {
|
|
184
|
+
const avatarName = value.openingElement.name.name;
|
|
185
|
+
if (avatarImports[avatarName]) {
|
|
186
|
+
return avatarName;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for JSX expression container: {<Avatar ... />}
|
|
191
|
+
if (value.type === 'JSXExpressionContainer' && value.expression) {
|
|
192
|
+
// Direct JSX element: {<Avatar ... />}
|
|
193
|
+
if (value.expression.type === 'JSXElement' && value.expression.openingElement.name.name === 'Avatar') {
|
|
194
|
+
const avatarName = value.expression.openingElement.name.name;
|
|
195
|
+
if (avatarImports[avatarName]) {
|
|
196
|
+
return avatarName;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Arrow function: {() => <Avatar ... />}
|
|
201
|
+
if (value.expression.type === 'ArrowFunctionExpression') {
|
|
202
|
+
const body = value.expression.body;
|
|
203
|
+
if (body.type === 'JSXElement' && body.openingElement.name.name === 'Avatar') {
|
|
204
|
+
const avatarName = body.openingElement.name.name;
|
|
205
|
+
if (avatarImports[avatarName]) {
|
|
206
|
+
return avatarName;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if color prop value needs mapping
|
|
216
|
+
*/
|
|
217
|
+
function colorNeedsMapping(colorProp) {
|
|
218
|
+
if (!(colorProp !== null && colorProp !== void 0 && colorProp.value)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const stringValue = extractStringValue(colorProp.value);
|
|
222
|
+
return stringValue !== null && typeof stringValue === 'string' && mapTagColorValue(stringValue) !== stringValue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract the string value from a JSX attribute value
|
|
227
|
+
*/
|
|
228
|
+
function extractStringValue(attrValue) {
|
|
229
|
+
if (!attrValue) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (attrValue.type === 'Literal') {
|
|
233
|
+
return attrValue.value;
|
|
234
|
+
}
|
|
235
|
+
if (attrValue.type === 'JSXExpressionContainer' && attrValue.expression && attrValue.expression.type === 'Literal') {
|
|
236
|
+
return attrValue.expression.value;
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create a fixer function to replace an appearance prop value
|
|
243
|
+
* Handles both Literal and JSXExpressionContainer with Literal
|
|
244
|
+
*/
|
|
245
|
+
function createAppearanceFixer(attrValue, newValue) {
|
|
246
|
+
return fixer => {
|
|
247
|
+
if (!attrValue) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
if (attrValue.type === 'Literal') {
|
|
251
|
+
return fixer.replaceText(attrValue, `"${newValue}"`);
|
|
252
|
+
}
|
|
253
|
+
if (attrValue.type === 'JSXExpressionContainer' && 'expression' in attrValue && attrValue.expression && attrValue.expression.type === 'Literal') {
|
|
254
|
+
return fixer.replaceText(attrValue.expression, `"${newValue}"`);
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Generate the replacement JSX element text for Tag migration
|
|
262
|
+
* Handles both regular Tag and avatarTag migrations
|
|
263
|
+
*/
|
|
264
|
+
function generateTagReplacement(node, options = {}) {
|
|
265
|
+
const sourceCode = context.getSourceCode();
|
|
266
|
+
const attributes = node.openingElement.attributes;
|
|
267
|
+
|
|
268
|
+
// Build new attributes array
|
|
269
|
+
const newAttributes = [];
|
|
270
|
+
attributes.forEach(attr => {
|
|
271
|
+
if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
|
|
272
|
+
const attrName = attr.name.name;
|
|
273
|
+
if (attrName === 'isBold') {
|
|
274
|
+
// Skip isBold attribute
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (attrName === 'appearance') {
|
|
278
|
+
// For Lozenge migrations, convert appearance to color prop
|
|
279
|
+
// For SimpleTag/RemovableTag migrations, delete appearance prop
|
|
280
|
+
if (options.isLozengeMigration) {
|
|
281
|
+
// Map Lozenge appearance value to Tag color value and change prop name from appearance to color
|
|
282
|
+
const stringValue = extractStringValue(attr.value);
|
|
283
|
+
if (stringValue && typeof stringValue === 'string') {
|
|
284
|
+
const mappedColor = mapLozengeAppearanceToTagColor(stringValue);
|
|
285
|
+
newAttributes.push(`color="${mappedColor}"`);
|
|
286
|
+
}
|
|
287
|
+
// If we can't extract the string value (dynamic expression), skip it
|
|
288
|
+
// Dynamic expressions should be caught earlier and require manual review
|
|
289
|
+
// This code path shouldn't be reached, but we skip to be safe
|
|
290
|
+
}
|
|
291
|
+
// For SimpleTag/RemovableTag migrations, skip appearance prop (delete it)
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (attrName === 'color') {
|
|
295
|
+
// For avatar tag, skip color prop; for regular tag, map color value
|
|
296
|
+
// Note: Lozenge doesn't have a color prop, but Tag/SimpleTag/RemovableTag do
|
|
297
|
+
if (options.isAvatarTag) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const stringValue = extractStringValue(attr.value);
|
|
301
|
+
if (stringValue && typeof stringValue === 'string') {
|
|
302
|
+
const mappedColor = mapTagColorValue(stringValue);
|
|
303
|
+
newAttributes.push(`color="${mappedColor}"`);
|
|
304
|
+
} else {
|
|
305
|
+
// If we can't extract the string value, keep as-is
|
|
306
|
+
const value = attr.value ? sourceCode.getText(attr.value) : '';
|
|
307
|
+
newAttributes.push(`color${value ? `=${value}` : ''}`);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (attrName === 'elemBefore') {
|
|
312
|
+
// For avatar tag, rename elemBefore to avatar and use render props
|
|
313
|
+
if (options.isAvatarTag) {
|
|
314
|
+
const elemBeforeValue = attr.value;
|
|
315
|
+
let avatarElement = null;
|
|
316
|
+
|
|
317
|
+
// Extract Avatar element from various formats
|
|
318
|
+
if (elemBeforeValue.type === 'JSXElement') {
|
|
319
|
+
avatarElement = elemBeforeValue;
|
|
320
|
+
} else if (elemBeforeValue.type === 'JSXExpressionContainer') {
|
|
321
|
+
const expr = elemBeforeValue.expression;
|
|
322
|
+
// Direct JSX element: {<Avatar ... />}
|
|
323
|
+
if (expr.type === 'JSXElement') {
|
|
324
|
+
avatarElement = expr;
|
|
325
|
+
}
|
|
326
|
+
// Arrow function: {() => <Avatar ... />}
|
|
327
|
+
else if (expr.type === 'ArrowFunctionExpression' && expr.body.type === 'JSXElement') {
|
|
328
|
+
avatarElement = expr.body;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (avatarElement) {
|
|
332
|
+
// Generate render props: avatar={(props) => <Avatar {...props} ... />}
|
|
333
|
+
const avatarElementText = sourceCode.getText(avatarElement);
|
|
334
|
+
// Add {...props} spread to the Avatar element attributes
|
|
335
|
+
const avatarWithProps = avatarElementText.replace(/<Avatar\s/, '<Avatar {...props} ');
|
|
336
|
+
newAttributes.push(`avatar={(props) => ${avatarWithProps}}`);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// For regular tag, keep elemBefore as-is
|
|
341
|
+
newAttributes.push(sourceCode.getText(attr));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Keep all other attributes
|
|
346
|
+
newAttributes.push(sourceCode.getText(attr));
|
|
347
|
+
} else if (attr.type === 'JSXSpreadAttribute') {
|
|
348
|
+
// Keep spread attributes
|
|
349
|
+
newAttributes.push(sourceCode.getText(attr));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Add isRemovable={false} for SimpleTag migrations and Lozenge migrations
|
|
354
|
+
if (options.isSimpleTag || options.isLozengeMigration) {
|
|
355
|
+
newAttributes.push('isRemovable={false}');
|
|
356
|
+
}
|
|
357
|
+
const attributesText = newAttributes.length > 0 ? ` ${newAttributes.join(' ')}` : '';
|
|
358
|
+
const children = node.children.length > 0 ? sourceCode.getText().slice(node.openingElement.range[1], node.closingElement ? node.closingElement.range[0] : node.range[1]) : '';
|
|
359
|
+
const componentName = options.preserveComponentName ? node.openingElement.name.name : options.isAvatarTag ? 'AvatarTag' : 'Tag';
|
|
360
|
+
if (node.closingElement) {
|
|
361
|
+
return `<${componentName}${attributesText}>${children}</${componentName}>`;
|
|
362
|
+
} else {
|
|
363
|
+
return `<${componentName}${attributesText} />`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
ImportDeclaration(node) {
|
|
368
|
+
const moduleSource = node.source.value;
|
|
369
|
+
if (typeof moduleSource === 'string') {
|
|
370
|
+
// Track Lozenge imports
|
|
371
|
+
if (moduleSource === '@atlaskit/lozenge' || moduleSource.startsWith('@atlaskit/lozenge')) {
|
|
372
|
+
node.specifiers.forEach(spec => {
|
|
373
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
374
|
+
lozengeImports[spec.local.name] = moduleSource;
|
|
375
|
+
} else if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
376
|
+
if (spec.imported.name === 'Lozenge') {
|
|
377
|
+
lozengeImports[spec.local.name] = moduleSource;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// Track Badge imports
|
|
383
|
+
if (moduleSource === '@atlaskit/badge' || moduleSource.startsWith('@atlaskit/badge')) {
|
|
384
|
+
node.specifiers.forEach(spec => {
|
|
385
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
386
|
+
badgeImports[spec.local.name] = moduleSource;
|
|
387
|
+
} else if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
388
|
+
if (spec.imported.name === 'Badge' || spec.imported.name === 'default') {
|
|
389
|
+
badgeImports[spec.local.name] = moduleSource;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// Track Tag imports (SimpleTag, RemovableTag only - not the new Tag component)
|
|
395
|
+
if (moduleSource === '@atlaskit/tag' || moduleSource.startsWith('@atlaskit/tag')) {
|
|
396
|
+
node.specifiers.forEach(spec => {
|
|
397
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
398
|
+
// Check for default imports from subpaths and main package
|
|
399
|
+
if (moduleSource === '@atlaskit/tag/simple-tag') {
|
|
400
|
+
// Default import from @atlaskit/tag/simple-tag is a SimpleTag
|
|
401
|
+
tagImports[spec.local.name] = {
|
|
402
|
+
type: 'SimpleTag',
|
|
403
|
+
source: moduleSource,
|
|
404
|
+
node: {
|
|
405
|
+
...spec,
|
|
406
|
+
parent: node
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
importDeclarationsToUpdate.add(node);
|
|
410
|
+
} else if (moduleSource === '@atlaskit/tag/removable-tag' || moduleSource === '@atlaskit/tag') {
|
|
411
|
+
// Default import from @atlaskit/tag/removable-tag or @atlaskit/tag is a RemovableTag
|
|
412
|
+
tagImports[spec.local.name] = {
|
|
413
|
+
type: 'RemovableTag',
|
|
414
|
+
source: moduleSource,
|
|
415
|
+
node: {
|
|
416
|
+
...spec,
|
|
417
|
+
parent: node
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
importDeclarationsToUpdate.add(node);
|
|
421
|
+
}
|
|
422
|
+
} else if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
423
|
+
const importName = spec.imported.name;
|
|
424
|
+
if (importName === 'SimpleTag' || importName === 'RemovableTag') {
|
|
425
|
+
tagImports[spec.local.name] = {
|
|
426
|
+
type: importName,
|
|
427
|
+
source: moduleSource,
|
|
428
|
+
node: {
|
|
429
|
+
...spec,
|
|
430
|
+
parent: node
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
// Mark this import declaration for potential updates
|
|
434
|
+
importDeclarationsToUpdate.add(node);
|
|
435
|
+
} else if (importName === 'AvatarTag') {
|
|
436
|
+
// Track new AvatarTag component - it should not be migrated
|
|
437
|
+
newTagImports[spec.local.name] = moduleSource;
|
|
438
|
+
}
|
|
439
|
+
// Note: Tag from named imports is not skipped - it may still need migration
|
|
440
|
+
// (e.g., if it has appearance prop or other old props)
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
// Track Avatar imports
|
|
445
|
+
if (moduleSource === '@atlaskit/avatar' || moduleSource.startsWith('@atlaskit/avatar')) {
|
|
446
|
+
node.specifiers.forEach(spec => {
|
|
447
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
448
|
+
avatarImports[spec.local.name] = moduleSource;
|
|
449
|
+
} else if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
450
|
+
if (spec.imported.name === 'Avatar') {
|
|
451
|
+
avatarImports[spec.local.name] = moduleSource;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
JSXElement(node) {
|
|
459
|
+
if (!isNodeOfType(node, 'JSXElement')) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const elementName = node.openingElement.name.name;
|
|
466
|
+
|
|
467
|
+
// Skip new AvatarTag component - it should not be migrated
|
|
468
|
+
if (newTagImports[elementName]) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Handle SimpleTag, RemovableTag, and Tag migrations
|
|
473
|
+
if (tagImports[elementName]) {
|
|
474
|
+
const tagImportInfo = tagImports[elementName];
|
|
475
|
+
const attributesMap = getAttributesMap(node.openingElement.attributes);
|
|
476
|
+
const {
|
|
477
|
+
elemBefore: elemBeforeProp,
|
|
478
|
+
avatar: avatarProp,
|
|
479
|
+
appearance: appearanceProp,
|
|
480
|
+
color: colorProp
|
|
481
|
+
} = attributesMap;
|
|
482
|
+
|
|
483
|
+
// For default import from @atlaskit/tag, check if it's already the new Tag
|
|
484
|
+
if (tagImportInfo.type === 'RemovableTag' && tagImportInfo.source === '@atlaskit/tag') {
|
|
485
|
+
// If using avatar prop, it's already the new Tag
|
|
486
|
+
if (avatarProp) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Check if component name is already correct and nothing needs migration
|
|
491
|
+
if (elementName === 'Tag' || elementName === 'AvatarTag') {
|
|
492
|
+
const needsNameChange = false;
|
|
493
|
+
const needsMigration = needsNameChange || appearanceProp || colorNeedsMapping(colorProp);
|
|
494
|
+
if (!needsMigration) {
|
|
495
|
+
// Still need to check elemBefore for Avatar
|
|
496
|
+
if (elemBeforeProp) {
|
|
497
|
+
const hasAvatarInElemBefore = getAvatarComponentName(elemBeforeProp) !== null;
|
|
498
|
+
if (hasAvatarInElemBefore) {
|
|
499
|
+
// Has Avatar in elemBefore, needs migration to AvatarTag
|
|
500
|
+
} else {
|
|
501
|
+
// No Avatar, nothing to migrate
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
// No elemBefore, nothing to migrate
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// If we get here, something needs migration
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Determine migration target based on elemBefore containing Avatar
|
|
514
|
+
const hasAvatarInElemBefore = elemBeforeProp ? getAvatarComponentName(elemBeforeProp) !== null : false;
|
|
515
|
+
const migrationTarget = hasAvatarInElemBefore ? 'AvatarTag' : 'Tag';
|
|
516
|
+
|
|
517
|
+
// Record the migration target for this import
|
|
518
|
+
tagMigrationTargets[elementName] = migrationTarget;
|
|
519
|
+
|
|
520
|
+
// Migrate the JSX element
|
|
521
|
+
context.report({
|
|
522
|
+
node: node,
|
|
523
|
+
messageId: 'migrateTag',
|
|
524
|
+
fix: fixer => {
|
|
525
|
+
var _tagImportInfo$node, _tagImportInfo$node$p, _tagImportInfo$node$p2, _tagImportInfo$node2, _tagImportInfo$node2$, _tagImportInfo$node2$2, _tagImportInfo$node3, _tagImportInfo$node3$, _tagImportInfo$node3$2, _tagImportInfo$node4, _tagImportInfo$node5, _tagImportInfo$node5$, _tagImportInfo$node5$2, _tagImportInfo$node6;
|
|
526
|
+
const fixes = [];
|
|
527
|
+
|
|
528
|
+
// Fix the JSX element
|
|
529
|
+
const replacement = generateTagReplacement(node, {
|
|
530
|
+
isAvatarTag: hasAvatarInElemBefore,
|
|
531
|
+
isSimpleTag: !hasAvatarInElemBefore && tagImportInfo.type === 'SimpleTag'
|
|
532
|
+
});
|
|
533
|
+
fixes.push(fixer.replaceText(node, replacement));
|
|
534
|
+
|
|
535
|
+
// Fix the import statement for named imports, subpath default imports, and main package default imports
|
|
536
|
+
const isSubpathImport = ((_tagImportInfo$node = tagImportInfo.node) === null || _tagImportInfo$node === void 0 ? void 0 : (_tagImportInfo$node$p = _tagImportInfo$node.parent) === null || _tagImportInfo$node$p === void 0 ? void 0 : (_tagImportInfo$node$p2 = _tagImportInfo$node$p.source) === null || _tagImportInfo$node$p2 === void 0 ? void 0 : _tagImportInfo$node$p2.value) === '@atlaskit/tag/simple-tag' || ((_tagImportInfo$node2 = tagImportInfo.node) === null || _tagImportInfo$node2 === void 0 ? void 0 : (_tagImportInfo$node2$ = _tagImportInfo$node2.parent) === null || _tagImportInfo$node2$ === void 0 ? void 0 : (_tagImportInfo$node2$2 = _tagImportInfo$node2$.source) === null || _tagImportInfo$node2$2 === void 0 ? void 0 : _tagImportInfo$node2$2.value) === '@atlaskit/tag/removable-tag';
|
|
537
|
+
const isMainPackageDefaultImport = ((_tagImportInfo$node3 = tagImportInfo.node) === null || _tagImportInfo$node3 === void 0 ? void 0 : (_tagImportInfo$node3$ = _tagImportInfo$node3.parent) === null || _tagImportInfo$node3$ === void 0 ? void 0 : (_tagImportInfo$node3$2 = _tagImportInfo$node3$.source) === null || _tagImportInfo$node3$2 === void 0 ? void 0 : _tagImportInfo$node3$2.value) === '@atlaskit/tag' && ((_tagImportInfo$node4 = tagImportInfo.node) === null || _tagImportInfo$node4 === void 0 ? void 0 : _tagImportInfo$node4.type) === 'ImportDefaultSpecifier';
|
|
538
|
+
if (isSubpathImport || isMainPackageDefaultImport || ((_tagImportInfo$node5 = tagImportInfo.node) === null || _tagImportInfo$node5 === void 0 ? void 0 : (_tagImportInfo$node5$ = _tagImportInfo$node5.parent) === null || _tagImportInfo$node5$ === void 0 ? void 0 : (_tagImportInfo$node5$2 = _tagImportInfo$node5$.source) === null || _tagImportInfo$node5$2 === void 0 ? void 0 : _tagImportInfo$node5$2.value) === '@atlaskit/tag' && ((_tagImportInfo$node6 = tagImportInfo.node) === null || _tagImportInfo$node6 === void 0 ? void 0 : _tagImportInfo$node6.type) === 'ImportSpecifier') {
|
|
539
|
+
var _tagImportInfo$node7;
|
|
540
|
+
const importNode = (_tagImportInfo$node7 = tagImportInfo.node) === null || _tagImportInfo$node7 === void 0 ? void 0 : _tagImportInfo$node7.parent;
|
|
541
|
+
if (importNode) {
|
|
542
|
+
const sourceCode = context.getSourceCode();
|
|
543
|
+
const mainModuleSource = '@atlaskit/tag';
|
|
544
|
+
|
|
545
|
+
// Get all other specifiers that are not SimpleTag or RemovableTag
|
|
546
|
+
// For subpath imports and main package default imports, exclude the default specifier itself
|
|
547
|
+
const otherSpecifiers = importNode.specifiers.filter(spec => {
|
|
548
|
+
// Skip default specifiers from subpath imports and main package - they're being replaced
|
|
549
|
+
if (spec.type === 'ImportDefaultSpecifier' && (isSubpathImport || isMainPackageDefaultImport)) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
553
|
+
const importName = spec.imported.name;
|
|
554
|
+
return importName !== 'SimpleTag' && importName !== 'RemovableTag';
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}).map(spec => sourceCode.getText(spec));
|
|
558
|
+
let newImportText = '';
|
|
559
|
+
if (migrationTarget === 'Tag') {
|
|
560
|
+
if (otherSpecifiers.length > 0) {
|
|
561
|
+
newImportText = `import Tag, { ${otherSpecifiers.join(', ')} } from '${mainModuleSource}';`;
|
|
562
|
+
} else {
|
|
563
|
+
newImportText = `import Tag from '${mainModuleSource}';`;
|
|
564
|
+
}
|
|
565
|
+
} else if (migrationTarget === 'AvatarTag') {
|
|
566
|
+
if (otherSpecifiers.length > 0) {
|
|
567
|
+
newImportText = `import { AvatarTag, ${otherSpecifiers.join(', ')} } from '${mainModuleSource}';`;
|
|
568
|
+
} else {
|
|
569
|
+
newImportText = `import { AvatarTag } from '${mainModuleSource}';`;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (newImportText) {
|
|
573
|
+
fixes.push(fixer.replaceText(importNode, newImportText));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return fixes.length === 1 ? fixes[0] : fixes;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Handle Badge components
|
|
584
|
+
if (badgeImports[elementName]) {
|
|
585
|
+
// Find the appearance prop
|
|
586
|
+
const appearanceProp = node.openingElement.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === 'appearance');
|
|
587
|
+
if (!appearanceProp || appearanceProp.type !== 'JSXAttribute') {
|
|
588
|
+
// No appearance prop or it's a spread attribute, nothing to migrate
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Check if it's a dynamic expression
|
|
593
|
+
if (isDynamicExpression(appearanceProp.value)) {
|
|
594
|
+
context.report({
|
|
595
|
+
node: appearanceProp,
|
|
596
|
+
messageId: 'dynamicBadgeAppearance'
|
|
597
|
+
});
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Extract the string value
|
|
602
|
+
const stringValue = extractStringValue(appearanceProp.value);
|
|
603
|
+
if (stringValue && typeof stringValue === 'string') {
|
|
604
|
+
const mappedValue = mapBadgeToNewAppearanceValue(stringValue);
|
|
605
|
+
if (mappedValue !== stringValue) {
|
|
606
|
+
context.report({
|
|
607
|
+
node: appearanceProp,
|
|
608
|
+
messageId: 'updateBadgeAppearance',
|
|
609
|
+
data: {
|
|
610
|
+
oldValue: stringValue,
|
|
611
|
+
newValue: mappedValue
|
|
612
|
+
},
|
|
613
|
+
fix: createAppearanceFixer(appearanceProp.value, mappedValue)
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Only process if this is a Lozenge component we've imported
|
|
621
|
+
if (!lozengeImports[elementName]) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const attributesMap = getAttributesMap(node.openingElement.attributes);
|
|
625
|
+
const appearanceProp = attributesMap.appearance;
|
|
626
|
+
const isBoldProp = attributesMap.isBold;
|
|
627
|
+
|
|
628
|
+
// Handle appearance prop value migration
|
|
629
|
+
if (appearanceProp) {
|
|
630
|
+
const shouldMigrateToTag = !isBoldProp || isLiteralFalse(isBoldProp.value);
|
|
631
|
+
if (!shouldMigrateToTag) {
|
|
632
|
+
// Only update appearance values for Lozenge components that stay as Lozenge
|
|
633
|
+
const stringValue = extractStringValue(appearanceProp.value);
|
|
634
|
+
if (stringValue && typeof stringValue === 'string') {
|
|
635
|
+
const mappedValue = mapToNewAppearanceValue(stringValue);
|
|
636
|
+
if (mappedValue !== stringValue) {
|
|
637
|
+
context.report({
|
|
638
|
+
node: appearanceProp,
|
|
639
|
+
messageId: 'updateAppearance',
|
|
640
|
+
fix: createAppearanceFixer(appearanceProp.value, mappedValue)
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Handle isBold prop and Tag migration
|
|
648
|
+
if (isBoldProp) {
|
|
649
|
+
if (isLiteralFalse(isBoldProp.value)) {
|
|
650
|
+
// isBold={false} should migrate to Tag
|
|
651
|
+
// Check if appearance is dynamic - if so, require manual review
|
|
652
|
+
if (appearanceProp && isDynamicExpression(appearanceProp.value)) {
|
|
653
|
+
context.report({
|
|
654
|
+
node: appearanceProp,
|
|
655
|
+
messageId: 'dynamicLozengeAppearance'
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
context.report({
|
|
660
|
+
node: node,
|
|
661
|
+
messageId: 'migrateTag',
|
|
662
|
+
fix: fixer => {
|
|
663
|
+
const replacement = generateTagReplacement(node, {
|
|
664
|
+
isLozengeMigration: true
|
|
665
|
+
});
|
|
666
|
+
return fixer.replaceText(node, replacement);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
} else if (isDynamicExpression(isBoldProp.value)) {
|
|
670
|
+
// Dynamic isBold requires manual review
|
|
671
|
+
context.report({
|
|
672
|
+
node: isBoldProp,
|
|
673
|
+
messageId: 'manualReview'
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
// isBold={true} or isBold (implicit true) - no action needed
|
|
677
|
+
} else {
|
|
678
|
+
// No isBold prop means implicit false, should migrate to Tag
|
|
679
|
+
// Check if appearance is dynamic - if so, require manual review
|
|
680
|
+
if (appearanceProp && isDynamicExpression(appearanceProp.value)) {
|
|
681
|
+
context.report({
|
|
682
|
+
node: appearanceProp,
|
|
683
|
+
messageId: 'dynamicLozengeAppearance'
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
context.report({
|
|
688
|
+
node: node,
|
|
689
|
+
messageId: 'migrateTag',
|
|
690
|
+
fix: fixer => {
|
|
691
|
+
const replacement = generateTagReplacement(node, {
|
|
692
|
+
isLozengeMigration: true
|
|
693
|
+
});
|
|
694
|
+
return fixer.replaceText(node, replacement);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
export default rule;
|