@sproutsocial/seeds-react-badge 1.0.6 → 2.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,489 @@
1
+ import type { API, FileInfo, Options, Collection } from "jscodeshift";
2
+
3
+ // Type to badgeColor mapping
4
+ const TYPE_TO_BADGE_COLOR = {
5
+ passive: "neutral",
6
+ default: "purple",
7
+ primary: "blue",
8
+ secondary: "yellow",
9
+ approval: "orange",
10
+ suggestion: "blue",
11
+ common: "aqua", // Note: colors.container.background.decorative.aqua does not match colors.aqua.600
12
+ } as const;
13
+
14
+ type BadgeType = keyof typeof TYPE_TO_BADGE_COLOR;
15
+ type BadgeColor = (typeof TYPE_TO_BADGE_COLOR)[BadgeType];
16
+
17
+ // Map type value to badgeColor value
18
+ function mapTypeToBadgeColor(typeValue: string): BadgeColor | undefined {
19
+ if (typeValue in TYPE_TO_BADGE_COLOR) {
20
+ return TYPE_TO_BADGE_COLOR[typeValue as BadgeType];
21
+ }
22
+ // Invalid type value - should not happen in practice
23
+ return undefined;
24
+ }
25
+
26
+ // Check if Badge is imported from racine or seeds-react-badge
27
+ function isBadgeComponent(
28
+ jsxElement: { value: { name: unknown } },
29
+ j: API["jscodeshift"]
30
+ ): boolean {
31
+ const name = jsxElement.value.name;
32
+ if (!name || typeof name !== "object") return false;
33
+
34
+ // Handle direct usage: <Badge />
35
+ if ("name" in name && (name as { name: string }).name === "Badge") {
36
+ return true;
37
+ }
38
+
39
+ // Handle member expressions: <Some.Badge />
40
+ if (
41
+ "type" in name &&
42
+ (name as { type: string }).type === "JSXMemberExpression" &&
43
+ "property" in name &&
44
+ (name as { property?: { name?: string } }).property?.name === "Badge"
45
+ ) {
46
+ return true;
47
+ }
48
+
49
+ return false;
50
+ }
51
+
52
+ // Find Badge import from racine or seeds-react-badge
53
+ function findBadgeImport(
54
+ root: Collection<any>,
55
+ j: API["jscodeshift"]
56
+ ): boolean {
57
+ const imports = root.find(j.ImportDeclaration);
58
+ let hasBadgeImport = false;
59
+
60
+ imports.forEach((path) => {
61
+ const source = path.value.source.value;
62
+ if (
63
+ source === "@sproutsocial/racine" ||
64
+ source === "@sproutsocial/seeds-react-badge"
65
+ ) {
66
+ const specifiers = path.value.specifiers || [];
67
+ specifiers.forEach((spec) => {
68
+ if (
69
+ (spec.type === "ImportSpecifier" &&
70
+ spec.imported?.name === "Badge") ||
71
+ spec.local?.name === "Badge"
72
+ ) {
73
+ hasBadgeImport = true;
74
+ }
75
+ });
76
+ }
77
+ });
78
+
79
+ return hasBadgeImport;
80
+ }
81
+
82
+ // Add TODO comment for data attribute references
83
+ function addDataAttributeTodo(
84
+ ast: Collection<any>,
85
+ j: API["jscodeshift"],
86
+ originalSource: string
87
+ ): boolean {
88
+ let hasModifications = false;
89
+
90
+ // Patterns to search for (case-insensitive, handle both single and double quotes)
91
+ const patterns = [
92
+ /data-qa-badge-type/i,
93
+ /data-qa-badge-tip/i,
94
+ /data-tip.*badge/i,
95
+ /badge-type/i,
96
+ /badge-tip/i,
97
+ /getByDataQaLabel\(['"]badge-type['"]/i,
98
+ /getByDataQaLabel\(['"]badge-tip['"]/i,
99
+ /queryByDataQaLabel\(['"]badge-type['"]/i,
100
+ /queryByDataQaLabel\(['"]badge-tip['"]/i,
101
+ /\.attr\(['"]data-tip['"]/i,
102
+ /\.attr\(['"]data-qa-badge/i,
103
+ /\[data-qa-badge-type\]/i,
104
+ /\[data-qa-badge-tip\]/i,
105
+ ];
106
+
107
+ // Check if any pattern matches
108
+ const hasMatch = patterns.some((pattern) => pattern.test(originalSource));
109
+
110
+ if (hasMatch) {
111
+ // Find the Program node to add comment
112
+ const program = ast.find(j.Program).paths()[0];
113
+ if (program) {
114
+ // Check if TODO comment already exists
115
+ const programBody = program.value.body;
116
+ const firstStatement = programBody[0];
117
+ const statementWithComments = firstStatement as {
118
+ leadingComments?: Array<{ value?: string }>;
119
+ };
120
+ const existingComment =
121
+ statementWithComments?.leadingComments?.some(
122
+ (comment: { value?: string }) =>
123
+ comment.value && comment.value.includes("badge-1.x-to-2.x codemod")
124
+ ) ||
125
+ program.value.comments?.some(
126
+ (comment) =>
127
+ comment.value && comment.value.includes("badge-1.x-to-2.x codemod")
128
+ );
129
+
130
+ if (!existingComment && firstStatement) {
131
+ const todoComment = j.commentBlock(
132
+ ` TODO (badge-1.x-to-2.x codemod): Data attributes data-qa-badge-type, data-qa-badge-tip, and data-tip have been removed from Badge component. Update test selectors and data attribute references. `,
133
+ true
134
+ );
135
+
136
+ // Add comment before the first statement using comments property (like icon-library codemod)
137
+ const statementWithComments = firstStatement as {
138
+ comments?: Array<{ value?: string }>;
139
+ };
140
+ statementWithComments.comments = statementWithComments.comments
141
+ ? [todoComment, ...statementWithComments.comments]
142
+ : [todoComment];
143
+ hasModifications = true;
144
+ }
145
+ }
146
+ }
147
+
148
+ return hasModifications;
149
+ }
150
+
151
+ function transformer(
152
+ file: FileInfo,
153
+ api: API,
154
+ options: Options
155
+ ): string | null {
156
+ const j = api.jscodeshift;
157
+ const root = j(file.source);
158
+ let hasModifications = false;
159
+
160
+ // Add TODO comments for data attribute references (check regardless of Badge imports)
161
+ // Use original file source for pattern matching
162
+ const dataAttrModifications = addDataAttributeTodo(root, j, file.source);
163
+ if (dataAttrModifications) {
164
+ hasModifications = true;
165
+ }
166
+
167
+ // Only process files that import Badge from racine or seeds-react-badge
168
+ const hasBadgeImport = findBadgeImport(root, j);
169
+
170
+ // If no Badge import, return unchanged (don't transform other Badge components)
171
+ if (!hasBadgeImport) {
172
+ return hasModifications ? root.toSource(options) : file.source;
173
+ }
174
+
175
+ // Find Badge JSX elements (find JSXElement, not just opening element)
176
+ const badgeElements = root.find(j.JSXElement).filter((path) => {
177
+ const openingElement = path.value.openingElement;
178
+ return openingElement && isBadgeComponent({ value: openingElement }, j);
179
+ });
180
+
181
+ // Transform Badge JSX elements
182
+ badgeElements.forEach((path) => {
183
+ const openingElement = path.value.openingElement;
184
+ if (!openingElement || !isBadgeComponent({ value: openingElement }, j))
185
+ return;
186
+
187
+ const attrs = openingElement.attributes || [];
188
+ const newAttrs: Array<(typeof attrs)[number]> = [];
189
+ let hasText = false;
190
+ let textValue: unknown = null;
191
+ let hasType = false;
192
+ let typeIsDynamic = false;
193
+ let typeStr: string | null = null; // Store extracted type value
194
+
195
+ // Process all attributes
196
+ attrs.forEach((attr) => {
197
+ if (attr.type === "JSXSpreadAttribute") {
198
+ // Keep spread attributes
199
+ newAttrs.push(attr);
200
+ return;
201
+ }
202
+
203
+ const attrName = attr.name.name;
204
+
205
+ // Handle size="default" -> size="large"
206
+ if (attrName === "size") {
207
+ let sizeValue: string | null = null;
208
+
209
+ if (
210
+ attr.value &&
211
+ (attr.value.type === "Literal" ||
212
+ attr.value.type === "StringLiteral") &&
213
+ typeof attr.value.value === "string"
214
+ ) {
215
+ sizeValue = attr.value.value;
216
+ } else if (
217
+ attr.value &&
218
+ attr.value.type === "JSXExpressionContainer" &&
219
+ attr.value.expression &&
220
+ (attr.value.expression.type === "Literal" ||
221
+ attr.value.expression.type === "StringLiteral") &&
222
+ typeof attr.value.expression.value === "string"
223
+ ) {
224
+ sizeValue = attr.value.expression.value;
225
+ }
226
+
227
+ if (sizeValue === "default") {
228
+ newAttrs.push(
229
+ j.jsxAttribute(j.jsxIdentifier("size"), j.literal("large"))
230
+ );
231
+ hasModifications = true;
232
+ } else {
233
+ newAttrs.push(attr);
234
+ }
235
+ return;
236
+ }
237
+
238
+ // Handle type prop
239
+ if (attrName === "type") {
240
+ hasType = true;
241
+
242
+ // Check if it's a dynamic value (variable or function call)
243
+ if (attr.value) {
244
+ if (attr.value.type === "JSXExpressionContainer") {
245
+ const expr = attr.value.expression;
246
+ if (
247
+ expr &&
248
+ expr.type !== "Literal" &&
249
+ expr.type !== "StringLiteral"
250
+ ) {
251
+ typeIsDynamic = true;
252
+ }
253
+ } else if (
254
+ attr.value.type !== "Literal" &&
255
+ attr.value.type !== "StringLiteral"
256
+ ) {
257
+ typeIsDynamic = true;
258
+ }
259
+ }
260
+
261
+ // Map static type values - extract typeStr but don't add badgeColor yet
262
+ if (!typeIsDynamic && attr.value) {
263
+ if (
264
+ (attr.value.type === "Literal" ||
265
+ attr.value.type === "StringLiteral") &&
266
+ typeof attr.value.value === "string"
267
+ ) {
268
+ typeStr = attr.value.value;
269
+ } else if (
270
+ attr.value.type === "JSXExpressionContainer" &&
271
+ attr.value.expression &&
272
+ (attr.value.expression.type === "Literal" ||
273
+ attr.value.expression.type === "StringLiteral") &&
274
+ typeof attr.value.expression.value === "string"
275
+ ) {
276
+ typeStr = attr.value.expression.value;
277
+ }
278
+ }
279
+
280
+ // Don't add type prop to newAttrs - it's being removed
281
+ // Mark as modified since we're removing the type prop
282
+ hasModifications = true;
283
+ return;
284
+ }
285
+
286
+ // Handle badgeColor - keep it if it exists
287
+ if (attrName === "badgeColor") {
288
+ newAttrs.push(attr);
289
+ return;
290
+ }
291
+
292
+ // Handle tip prop - remove it
293
+ if (attrName === "tip") {
294
+ hasModifications = true;
295
+ return; // Don't add to newAttrs
296
+ }
297
+
298
+ // Handle text prop
299
+ if (attrName === "text") {
300
+ hasText = true;
301
+ textValue = attr.value;
302
+ hasModifications = true;
303
+ // Don't add text prop to newAttrs - we'll convert to children
304
+ return;
305
+ }
306
+
307
+ // Keep all other attributes
308
+ newAttrs.push(attr);
309
+ });
310
+
311
+ // After processing all attributes, add badgeColor if needed
312
+ if (hasType && !typeIsDynamic && typeStr) {
313
+ const badgeColor = mapTypeToBadgeColor(typeStr);
314
+
315
+ if (badgeColor !== undefined) {
316
+ // Check if badgeColor already exists in newAttrs
317
+ const existingBadgeColor = newAttrs.find(
318
+ (a) => a.type !== "JSXSpreadAttribute" && a.name.name === "badgeColor"
319
+ );
320
+
321
+ if (!existingBadgeColor) {
322
+ newAttrs.push(
323
+ j.jsxAttribute(j.jsxIdentifier("badgeColor"), j.literal(badgeColor))
324
+ );
325
+ hasModifications = true;
326
+ }
327
+
328
+ // Special handling for "common" type - add TODO comment
329
+ if (typeStr === "common") {
330
+ const parentPath = path.parentPath;
331
+ if (parentPath && parentPath.value) {
332
+ const parentNode = parentPath.value as {
333
+ comments?: Array<{ value?: string }>;
334
+ };
335
+ const todoComment = j.commentBlock(
336
+ ` TODO (badge-1.x-to-2.x codemod): type="common" mapped to badgeColor="aqua", but colors.container.background.decorative.aqua does not match original colors.aqua.600. Verify color match. `,
337
+ true
338
+ );
339
+ parentNode.comments = parentNode.comments
340
+ ? [todoComment, ...parentNode.comments]
341
+ : [todoComment];
342
+ hasModifications = true;
343
+ }
344
+ }
345
+ } else {
346
+ // Add TODO comment for unmapped static type values
347
+ const parentPath = path.parentPath;
348
+ if (parentPath && parentPath.value) {
349
+ const parentNode = parentPath.value as {
350
+ comments?: Array<{ value?: string }>;
351
+ };
352
+ const todoComment = j.commentBlock(
353
+ ` TODO (badge-1.x-to-2.x codemod): Unmapped static type value "${typeStr}". Manually update to a valid badgeColor. `,
354
+ true
355
+ );
356
+ parentNode.comments = parentNode.comments
357
+ ? [todoComment, ...parentNode.comments]
358
+ : [todoComment];
359
+ hasModifications = true;
360
+ }
361
+ }
362
+ }
363
+
364
+ // Update attributes
365
+ openingElement.attributes = newAttrs;
366
+
367
+ // Handle dynamic type - add TODO comment
368
+ if (hasType && typeIsDynamic) {
369
+ // Traverse up to find the containing statement (not just immediate parent)
370
+ let currentPath = path.parentPath;
371
+ let statementNode: { comments?: Array<{ value?: string }> } | null = null;
372
+
373
+ // Keep traversing up until we find a statement or reach the Program
374
+ while (currentPath) {
375
+ const node = currentPath.value;
376
+ // Check if this is a statement (not JSX)
377
+ if (
378
+ node &&
379
+ typeof node === "object" &&
380
+ "type" in node &&
381
+ node.type !== "JSXElement" &&
382
+ node.type !== "JSXFragment" &&
383
+ node.type !== "JSXExpressionContainer"
384
+ ) {
385
+ statementNode = node as { comments?: Array<{ value?: string }> };
386
+ break;
387
+ }
388
+ currentPath = currentPath.parentPath;
389
+ }
390
+
391
+ // If we didn't find a statement, try to add to the JSXElement's parent
392
+ if (!statementNode) {
393
+ const parentPath = path.parentPath;
394
+ if (parentPath && parentPath.value) {
395
+ statementNode = parentPath.value as {
396
+ comments?: Array<{ value?: string }>;
397
+ };
398
+ }
399
+ }
400
+
401
+ if (statementNode) {
402
+ const todoComment = j.commentBlock(
403
+ ` TODO (badge-1.x-to-2.x codemod): Update function/variable to return badgeColor values instead of type values. See type-to-badgeColor mapping: passive→neutral, default→purple, primary→blue, secondary→yellow, approval→orange, suggestion→blue, common→aqua. `,
404
+ true
405
+ );
406
+ statementNode.comments = statementNode.comments
407
+ ? [todoComment, ...statementNode.comments]
408
+ : [todoComment];
409
+ hasModifications = true;
410
+ }
411
+ }
412
+
413
+ // Handle text prop -> children conversion
414
+ if (hasText) {
415
+ // path is already the JSXElement
416
+ // Only convert if there are no existing children
417
+ if (!path.value.children || path.value.children.length === 0) {
418
+ if (textValue) {
419
+ let childNodes: unknown[] = [];
420
+
421
+ // Handle different text value types
422
+ if (
423
+ textValue &&
424
+ typeof textValue === "object" &&
425
+ "type" in textValue
426
+ ) {
427
+ const textValueWithType = textValue as {
428
+ type: string;
429
+ expression?: unknown;
430
+ value?: unknown;
431
+ };
432
+ if (textValueWithType.type === "Literal") {
433
+ childNodes = [j.jsxText(String(textValueWithType.value))];
434
+ } else if (textValueWithType.type === "JSXExpressionContainer") {
435
+ // Extract the expression from the container
436
+ if (textValueWithType.expression) {
437
+ if (
438
+ typeof textValueWithType.expression === "object" &&
439
+ "type" in textValueWithType.expression &&
440
+ (textValueWithType.expression as { type: string }).type ===
441
+ "JSXElement"
442
+ ) {
443
+ // JSX element inside expression container - extract it
444
+ childNodes = [textValueWithType.expression];
445
+ } else {
446
+ // Keep the expression container
447
+ childNodes = [textValue];
448
+ }
449
+ } else {
450
+ childNodes = [textValue];
451
+ }
452
+ } else if (textValueWithType.type === "JSXElement") {
453
+ // Direct JSX element
454
+ childNodes = [textValue];
455
+ } else {
456
+ // Wrap in expression container
457
+ childNodes = [j.jsxExpressionContainer(textValue as any)];
458
+ }
459
+ } else {
460
+ // Wrap in expression container
461
+ childNodes = [j.jsxExpressionContainer(textValue as any)];
462
+ }
463
+
464
+ // Create a new opening element with updated attributes
465
+ const newOpeningElement = j.jsxOpeningElement(
466
+ openingElement.name,
467
+ newAttrs
468
+ );
469
+
470
+ // Create a new JSXElement with children and closing element
471
+ const newJSXElement = j.jsxElement(
472
+ newOpeningElement,
473
+ j.jsxClosingElement(openingElement.name),
474
+ childNodes as Parameters<typeof j.jsxElement>[2]
475
+ );
476
+
477
+ // Replace the old element with the new one
478
+ path.replace(newJSXElement);
479
+
480
+ hasModifications = true;
481
+ }
482
+ }
483
+ }
484
+ });
485
+
486
+ return hasModifications ? root.toSource(options) : file.source;
487
+ }
488
+
489
+ module.exports = transformer;
@@ -0,0 +1,2 @@
1
+
2
+ export { }