@carbon/upgrade 11.41.0 → 11.42.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/cli.js +330 -142
- package/package.json +2 -2
- package/transforms/__testfixtures__/ibm-products-update-tearsheet-alias.input.tsx +22 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet-alias.output.tsx +27 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet-edge-cases.input.tsx +112 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet-edge-cases.output.tsx +193 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet-idempotent.input.tsx +18 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet-idempotent.output.tsx +18 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet.input.tsx +202 -0
- package/transforms/__testfixtures__/ibm-products-update-tearsheet.output.tsx +289 -0
- package/transforms/__tests__/ibm-products-update-tearsheet-test.js +30 -0
- package/transforms/ibm-products-update-tearsheet.js +442 -0
- package/transforms/nonStableMapping.js +1 -1
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2024, 2025
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* Transforms Tearsheet to preview__Tearsheet with composable API:
|
|
8
|
+
*
|
|
9
|
+
* Before:
|
|
10
|
+
* <Tearsheet
|
|
11
|
+
* title="Title"
|
|
12
|
+
* label="Label"
|
|
13
|
+
* actions={[...]}
|
|
14
|
+
* influencer={<Component />}
|
|
15
|
+
* >
|
|
16
|
+
* {children}
|
|
17
|
+
* </Tearsheet>
|
|
18
|
+
*
|
|
19
|
+
* After:
|
|
20
|
+
* <Tearsheet>
|
|
21
|
+
* <Tearsheet.Header>
|
|
22
|
+
* <Tearsheet.HeaderContent title="Title" label="Label" />
|
|
23
|
+
* </Tearsheet.Header>
|
|
24
|
+
* <Tearsheet.Influencer><Component /></Tearsheet.Influencer>
|
|
25
|
+
* <Tearsheet.Body>
|
|
26
|
+
* <Tearsheet.MainContent>{children}</Tearsheet.MainContent>
|
|
27
|
+
* </Tearsheet.Body>
|
|
28
|
+
* <Tearsheet.Footer actions={[...]} />
|
|
29
|
+
* </Tearsheet>
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
// Props that stay on the root Tearsheet component
|
|
35
|
+
const ROOT_PROPS = [
|
|
36
|
+
'open',
|
|
37
|
+
'onClose',
|
|
38
|
+
'variant',
|
|
39
|
+
'decorator',
|
|
40
|
+
'influencerWidth',
|
|
41
|
+
'summaryContentWidth',
|
|
42
|
+
'verticalGap',
|
|
43
|
+
'preventCloseOnClickOutside',
|
|
44
|
+
'launcherButtonRef',
|
|
45
|
+
'selectorPrimaryFocus',
|
|
46
|
+
'keepMounted',
|
|
47
|
+
'disablePortal',
|
|
48
|
+
'className',
|
|
49
|
+
'portalTarget',
|
|
50
|
+
'height',
|
|
51
|
+
'hasCloseIcon',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Props that move to Tearsheet.HeaderContent
|
|
55
|
+
const HEADER_CONTENT_PROPS = ['title', 'label', 'description', 'headerActions'];
|
|
56
|
+
|
|
57
|
+
// Props that move to Tearsheet.Header
|
|
58
|
+
const HEADER_PROPS = [
|
|
59
|
+
'hideCloseButton',
|
|
60
|
+
'disableHeaderCollapse',
|
|
61
|
+
'closeIconDescription',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Props that move to Tearsheet.Footer
|
|
65
|
+
const FOOTER_PROPS = ['actions'];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Helper to create JSX member expression (e.g., Tearsheet.Header)
|
|
69
|
+
*/
|
|
70
|
+
function createMemberExpression(j, localName, property) {
|
|
71
|
+
return j.jsxMemberExpression(
|
|
72
|
+
j.jsxIdentifier(localName),
|
|
73
|
+
j.jsxIdentifier(property)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Helper to extract JSX value from attribute
|
|
79
|
+
*/
|
|
80
|
+
function extractJSXValue(attributeValue) {
|
|
81
|
+
if (!attributeValue) return null;
|
|
82
|
+
|
|
83
|
+
if (attributeValue.type === 'JSXExpressionContainer') {
|
|
84
|
+
return attributeValue.expression;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return attributeValue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create Tearsheet.Header with HeaderContent
|
|
92
|
+
*/
|
|
93
|
+
function createHeader(j, localName, headerProps, headerContentProps) {
|
|
94
|
+
const hasHeaderContentProps = headerContentProps.length > 0;
|
|
95
|
+
const hasHeaderProps = headerProps.length > 0;
|
|
96
|
+
|
|
97
|
+
if (!hasHeaderContentProps && !hasHeaderProps) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Create HeaderContent
|
|
102
|
+
const headerContent = j.jsxElement(
|
|
103
|
+
j.jsxOpeningElement(
|
|
104
|
+
createMemberExpression(j, localName, 'HeaderContent'),
|
|
105
|
+
headerContentProps,
|
|
106
|
+
headerContentProps.length === 0
|
|
107
|
+
),
|
|
108
|
+
headerContentProps.length === 0
|
|
109
|
+
? null
|
|
110
|
+
: j.jsxClosingElement(
|
|
111
|
+
createMemberExpression(j, localName, 'HeaderContent')
|
|
112
|
+
),
|
|
113
|
+
[]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Create Header wrapping HeaderContent
|
|
117
|
+
const header = j.jsxElement(
|
|
118
|
+
j.jsxOpeningElement(
|
|
119
|
+
createMemberExpression(j, localName, 'Header'),
|
|
120
|
+
headerProps
|
|
121
|
+
),
|
|
122
|
+
j.jsxClosingElement(createMemberExpression(j, localName, 'Header')),
|
|
123
|
+
[j.jsxText('\n '), headerContent, j.jsxText('\n ')]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return header;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create Tearsheet.Influencer
|
|
131
|
+
*/
|
|
132
|
+
function createInfluencer(j, localName, influencerValue) {
|
|
133
|
+
if (!influencerValue) return null;
|
|
134
|
+
|
|
135
|
+
const content = extractJSXValue(influencerValue);
|
|
136
|
+
if (!content) return null;
|
|
137
|
+
|
|
138
|
+
return j.jsxElement(
|
|
139
|
+
j.jsxOpeningElement(createMemberExpression(j, localName, 'Influencer'), []),
|
|
140
|
+
j.jsxClosingElement(createMemberExpression(j, localName, 'Influencer')),
|
|
141
|
+
[j.jsxText('\n '), content, j.jsxText('\n ')]
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create Tearsheet.NavigationBar
|
|
147
|
+
*/
|
|
148
|
+
function createNavigationBar(j, localName, navigationValue) {
|
|
149
|
+
if (!navigationValue) return null;
|
|
150
|
+
|
|
151
|
+
const content = extractJSXValue(navigationValue);
|
|
152
|
+
if (!content) return null;
|
|
153
|
+
|
|
154
|
+
return j.jsxElement(
|
|
155
|
+
j.jsxOpeningElement(
|
|
156
|
+
createMemberExpression(j, localName, 'NavigationBar'),
|
|
157
|
+
[]
|
|
158
|
+
),
|
|
159
|
+
j.jsxClosingElement(createMemberExpression(j, localName, 'NavigationBar')),
|
|
160
|
+
[j.jsxText('\n '), content, j.jsxText('\n ')]
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create Tearsheet.Body with MainContent
|
|
166
|
+
*/
|
|
167
|
+
function createBody(j, localName, originalChildren) {
|
|
168
|
+
// Create MainContent with original children
|
|
169
|
+
const mainContent = j.jsxElement(
|
|
170
|
+
j.jsxOpeningElement(
|
|
171
|
+
createMemberExpression(j, localName, 'MainContent'),
|
|
172
|
+
[]
|
|
173
|
+
),
|
|
174
|
+
j.jsxClosingElement(createMemberExpression(j, localName, 'MainContent')),
|
|
175
|
+
originalChildren.length > 0
|
|
176
|
+
? [j.jsxText('\n '), ...originalChildren, j.jsxText('\n ')]
|
|
177
|
+
: []
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Create Body wrapping MainContent
|
|
181
|
+
const body = j.jsxElement(
|
|
182
|
+
j.jsxOpeningElement(createMemberExpression(j, localName, 'Body'), []),
|
|
183
|
+
j.jsxClosingElement(createMemberExpression(j, localName, 'Body')),
|
|
184
|
+
[j.jsxText('\n '), mainContent, j.jsxText('\n ')]
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return body;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create Tearsheet.Footer
|
|
192
|
+
*/
|
|
193
|
+
function createFooter(j, localName, footerProps) {
|
|
194
|
+
if (footerProps.length === 0) return null;
|
|
195
|
+
|
|
196
|
+
return j.jsxElement(
|
|
197
|
+
j.jsxOpeningElement(
|
|
198
|
+
createMemberExpression(j, localName, 'Footer'),
|
|
199
|
+
footerProps,
|
|
200
|
+
true // self-closing
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Transform Tearsheet JSX element to composable structure
|
|
207
|
+
*/
|
|
208
|
+
function transformTearsheetElement(j, path, localName) {
|
|
209
|
+
const element = path.node;
|
|
210
|
+
const openingElement = element.openingElement;
|
|
211
|
+
const attributes = openingElement.attributes || [];
|
|
212
|
+
const originalChildren = element.children || [];
|
|
213
|
+
|
|
214
|
+
// Categorize props
|
|
215
|
+
const rootProps = [];
|
|
216
|
+
const headerProps = [];
|
|
217
|
+
const headerContentProps = [];
|
|
218
|
+
const footerProps = [];
|
|
219
|
+
let influencerValue = null;
|
|
220
|
+
let navigationValue = null;
|
|
221
|
+
|
|
222
|
+
attributes.forEach((attr) => {
|
|
223
|
+
if (attr.type !== 'JSXAttribute') {
|
|
224
|
+
// Handle spread attributes
|
|
225
|
+
rootProps.push(attr);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const propName = attr.name.name;
|
|
230
|
+
|
|
231
|
+
// Handle deprecated props
|
|
232
|
+
if (propName === 'slug') {
|
|
233
|
+
// Rename slug to decorator
|
|
234
|
+
attr.name.name = 'decorator';
|
|
235
|
+
rootProps.push(attr);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (propName === 'influencerPosition') {
|
|
240
|
+
// Skip - no longer supported
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Categorize props
|
|
245
|
+
if (ROOT_PROPS.includes(propName)) {
|
|
246
|
+
rootProps.push(attr);
|
|
247
|
+
} else if (HEADER_PROPS.includes(propName)) {
|
|
248
|
+
headerProps.push(attr);
|
|
249
|
+
} else if (HEADER_CONTENT_PROPS.includes(propName)) {
|
|
250
|
+
headerContentProps.push(attr);
|
|
251
|
+
} else if (propName === 'influencer') {
|
|
252
|
+
influencerValue = attr.value;
|
|
253
|
+
} else if (propName === 'navigation') {
|
|
254
|
+
navigationValue = attr.value;
|
|
255
|
+
} else if (FOOTER_PROPS.includes(propName)) {
|
|
256
|
+
footerProps.push(attr);
|
|
257
|
+
} else {
|
|
258
|
+
// Unknown props stay on root
|
|
259
|
+
rootProps.push(attr);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Build new composable structure
|
|
264
|
+
const newChildren = [];
|
|
265
|
+
|
|
266
|
+
// Add Header (if needed)
|
|
267
|
+
const header = createHeader(j, localName, headerProps, headerContentProps);
|
|
268
|
+
if (header) {
|
|
269
|
+
newChildren.push(j.jsxText('\n '), header);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add Influencer (if needed)
|
|
273
|
+
const influencer = createInfluencer(j, localName, influencerValue);
|
|
274
|
+
if (influencer) {
|
|
275
|
+
newChildren.push(j.jsxText('\n '), influencer);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add NavigationBar (if needed)
|
|
279
|
+
const navigationBar = createNavigationBar(j, localName, navigationValue);
|
|
280
|
+
if (navigationBar) {
|
|
281
|
+
newChildren.push(j.jsxText('\n '), navigationBar);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Add Body (always)
|
|
285
|
+
const body = createBody(j, localName, originalChildren);
|
|
286
|
+
newChildren.push(j.jsxText('\n '), body);
|
|
287
|
+
|
|
288
|
+
// Add Footer (if needed)
|
|
289
|
+
const footer = createFooter(j, localName, footerProps);
|
|
290
|
+
if (footer) {
|
|
291
|
+
newChildren.push(j.jsxText('\n '), footer);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Add final newline
|
|
295
|
+
if (newChildren.length > 0) {
|
|
296
|
+
newChildren.push(j.jsxText('\n'));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Create new Tearsheet element with composable structure
|
|
300
|
+
const newElement = j.jsxElement(
|
|
301
|
+
j.jsxOpeningElement(j.jsxIdentifier(localName), rootProps),
|
|
302
|
+
j.jsxClosingElement(j.jsxIdentifier(localName)),
|
|
303
|
+
newChildren
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
return newElement;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function transform(fileInfo, api) {
|
|
310
|
+
const j = api.jscodeshift;
|
|
311
|
+
const root = j(fileInfo.source);
|
|
312
|
+
|
|
313
|
+
let jsxTransformed = false;
|
|
314
|
+
|
|
315
|
+
// Track local names for Tearsheet imports and their original imported names
|
|
316
|
+
const tearsheetLocalNames = new Map(); // localName -> importedName
|
|
317
|
+
|
|
318
|
+
// First pass: identify Tearsheet imports and track local names
|
|
319
|
+
root
|
|
320
|
+
.find(j.ImportDeclaration, {
|
|
321
|
+
source: {
|
|
322
|
+
value: '@carbon/ibm-products',
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
.forEach((path) => {
|
|
326
|
+
path.node.specifiers.forEach((specifier) => {
|
|
327
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
328
|
+
const importedName = specifier.imported.name;
|
|
329
|
+
const localName = specifier.local
|
|
330
|
+
? specifier.local.name
|
|
331
|
+
: importedName;
|
|
332
|
+
|
|
333
|
+
// Track Tearsheet imports
|
|
334
|
+
if (
|
|
335
|
+
importedName === 'Tearsheet' ||
|
|
336
|
+
importedName === 'preview__Tearsheet'
|
|
337
|
+
) {
|
|
338
|
+
tearsheetLocalNames.set(localName, importedName);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Second pass: Transform Tearsheet JSX elements
|
|
345
|
+
root.find(j.JSXElement).forEach((path) => {
|
|
346
|
+
const openingElement = path.node.openingElement;
|
|
347
|
+
if (openingElement.name.type === 'JSXIdentifier') {
|
|
348
|
+
const elementName = openingElement.name.name;
|
|
349
|
+
|
|
350
|
+
// Check if this element uses a Tearsheet local name
|
|
351
|
+
if (tearsheetLocalNames.has(elementName)) {
|
|
352
|
+
// Check if already using composable API (idempotency check)
|
|
353
|
+
const hasComposableChildren = path.node.children?.some((child) => {
|
|
354
|
+
if (child.type === 'JSXElement') {
|
|
355
|
+
const childName = child.openingElement.name;
|
|
356
|
+
if (childName.type === 'JSXMemberExpression') {
|
|
357
|
+
const objectName = childName.object.name;
|
|
358
|
+
const propertyName = childName.property.name;
|
|
359
|
+
// Check if child is Tearsheet.Header, Tearsheet.Body, etc.
|
|
360
|
+
return (
|
|
361
|
+
objectName === elementName &&
|
|
362
|
+
[
|
|
363
|
+
'Header',
|
|
364
|
+
'Body',
|
|
365
|
+
'Footer',
|
|
366
|
+
'Influencer',
|
|
367
|
+
'NavigationBar',
|
|
368
|
+
].includes(propertyName)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Only transform if not already using composable API
|
|
376
|
+
if (!hasComposableChildren) {
|
|
377
|
+
const newElement = transformTearsheetElement(j, path, elementName);
|
|
378
|
+
j(path).replaceWith(newElement);
|
|
379
|
+
jsxTransformed = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Third pass: Only transform imports if JSX was transformed
|
|
386
|
+
if (jsxTransformed) {
|
|
387
|
+
root
|
|
388
|
+
.find(j.ImportDeclaration, {
|
|
389
|
+
source: {
|
|
390
|
+
value: '@carbon/ibm-products',
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
.forEach((path) => {
|
|
394
|
+
const seen = new Set();
|
|
395
|
+
const newSpecifiers = [];
|
|
396
|
+
|
|
397
|
+
path.node.specifiers.forEach((specifier) => {
|
|
398
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
399
|
+
const importedName = specifier.imported.name;
|
|
400
|
+
const localName = specifier.local
|
|
401
|
+
? specifier.local.name
|
|
402
|
+
: importedName;
|
|
403
|
+
|
|
404
|
+
// Check if this is Tearsheet
|
|
405
|
+
if (
|
|
406
|
+
importedName === 'Tearsheet' ||
|
|
407
|
+
importedName === 'preview__Tearsheet'
|
|
408
|
+
) {
|
|
409
|
+
// Transform to preview__Tearsheet as localName
|
|
410
|
+
const newImported = j.identifier('preview__Tearsheet');
|
|
411
|
+
const newLocal = j.identifier(localName);
|
|
412
|
+
const newSpecifier = j.importSpecifier(newImported, newLocal);
|
|
413
|
+
|
|
414
|
+
if (!seen.has(localName)) {
|
|
415
|
+
newSpecifiers.push(newSpecifier);
|
|
416
|
+
seen.add(localName);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
// Keep other imports as-is
|
|
420
|
+
if (!seen.has(localName)) {
|
|
421
|
+
newSpecifiers.push(specifier);
|
|
422
|
+
seen.add(localName);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
// Keep default imports and namespace imports
|
|
427
|
+
newSpecifiers.push(specifier);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
path.node.specifiers = newSpecifiers;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return root.toSource({ quote: 'single', trailingComma: true });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Return original source if no transformations were made
|
|
438
|
+
return fileInfo.source;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
module.exports = transform;
|
|
442
|
+
module.exports.parser = 'tsx';
|