@react-spectrum/codemods 3.0.0-nightly-796738501-260310 → 3.0.0-nightly-6a0d13c57-260322

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/dist/index.js CHANGED
@@ -25,6 +25,9 @@ const options = {
25
25
  },
26
26
  'components': {
27
27
  type: 'string'
28
+ },
29
+ 'agent': {
30
+ type: 'boolean'
28
31
  }
29
32
  };
30
33
  const { values, positionals } = parseArgs({
@@ -35,21 +38,21 @@ if (positionals.length < 1) {
35
38
  console.error('Please specify a codemod to run. Available codemods: ', Object.keys(codemods).join(', '));
36
39
  process.exit(1);
37
40
  }
38
- const codemodName = positionals[0];
39
- const codemodFunction = codemods[codemodName];
40
- if (!codemodFunction) {
41
- console.error(`Unknown codemod: ${codemodName}, available codemods: ${Object.keys(codemods).join(', ')}`);
42
- process.exit(1);
43
- }
44
- try {
45
- codemodFunction({
41
+ async function main() {
42
+ const codemodName = positionals[0];
43
+ const codemodFunction = codemods[codemodName];
44
+ if (!codemodFunction) {
45
+ console.error(`Unknown codemod: ${codemodName}, available codemods: ${Object.keys(codemods).join(', ')}`);
46
+ process.exit(1);
47
+ }
48
+ await Promise.resolve(codemodFunction({
46
49
  parser: 'tsx',
47
50
  ignorePattern: '**/node_modules/**',
48
51
  path: '.',
49
52
  ...values
50
- });
53
+ }));
51
54
  }
52
- catch (error) {
55
+ main().catch((error) => {
53
56
  console.error(`Error running codemod: ${error}`);
54
57
  process.exit(1);
55
- }
58
+ });
@@ -41,6 +41,7 @@ exports.default = transformer;
41
41
  const utils_1 = require("./shared/utils");
42
42
  const getComponents_1 = require("../getComponents");
43
43
  const iconMap_1 = require("./icons/iconMap");
44
+ const illustrationMap_1 = require("./illustrations/illustrationMap");
44
45
  const recast_1 = require("recast");
45
46
  const t = __importStar(require("@babel/types"));
46
47
  const styleProps_1 = __importDefault(require("./shared/styleProps"));
@@ -63,6 +64,108 @@ availableComponents.add('ActionGroup');
63
64
  let renamedComponents = {
64
65
  ContextualHelpTrigger: 'UnavailableMenuItemTrigger'
65
66
  };
67
+ const relatedComponentGroups = {
68
+ ActionMenu: {
69
+ scopedComponents: {
70
+ Item: ['ActionMenu']
71
+ }
72
+ },
73
+ Breadcrumbs: {
74
+ scopedComponents: {
75
+ Item: ['Breadcrumbs']
76
+ }
77
+ },
78
+ ComboBox: {
79
+ scopedComponents: {
80
+ Item: ['ComboBox'],
81
+ Section: ['ComboBox']
82
+ }
83
+ },
84
+ DialogContainer: {
85
+ components: ['Dialog']
86
+ },
87
+ DialogTrigger: {
88
+ components: ['Dialog']
89
+ },
90
+ Menu: {
91
+ components: ['ContextualHelpTrigger', 'MenuTrigger', 'SubmenuTrigger'],
92
+ scopedComponents: {
93
+ Item: ['Menu'],
94
+ Section: ['Menu']
95
+ }
96
+ },
97
+ Picker: {
98
+ scopedComponents: {
99
+ Item: ['Picker'],
100
+ Section: ['Picker']
101
+ }
102
+ },
103
+ TableView: {
104
+ components: ['Cell', 'Column', 'Row', 'TableBody', 'TableHeader']
105
+ },
106
+ Tabs: {
107
+ components: ['TabList', 'TabPanels']
108
+ },
109
+ TagGroup: {
110
+ scopedComponents: {
111
+ Item: ['TagGroup']
112
+ }
113
+ },
114
+ TooltipTrigger: {
115
+ components: ['Tooltip']
116
+ }
117
+ };
118
+ function addScopedParents(scopedComponents, component, parents) {
119
+ let existingParents = scopedComponents.get(component) ?? new Set();
120
+ for (let parent of parents) {
121
+ existingParents.add(parent);
122
+ }
123
+ scopedComponents.set(component, existingParents);
124
+ }
125
+ function getComponentSelection(components) {
126
+ if (!components) {
127
+ return {
128
+ components: new Set(availableComponents),
129
+ explicitComponents: new Set(availableComponents),
130
+ scopedComponents: new Map()
131
+ };
132
+ }
133
+ let explicitComponents = new Set(components.split(',').map(s => s.trim()).filter(Boolean));
134
+ let expandedComponents = new Set(explicitComponents);
135
+ let scopedComponents = new Map();
136
+ for (let component of explicitComponents) {
137
+ let relatedComponents = relatedComponentGroups[component];
138
+ if (!relatedComponents) {
139
+ continue;
140
+ }
141
+ for (let relatedComponent of relatedComponents.components ?? []) {
142
+ expandedComponents.add(relatedComponent);
143
+ }
144
+ for (let [relatedComponent, parents] of Object.entries(relatedComponents.scopedComponents ?? {})) {
145
+ expandedComponents.add(relatedComponent);
146
+ if (!explicitComponents.has(relatedComponent)) {
147
+ addScopedParents(scopedComponents, relatedComponent, parents);
148
+ }
149
+ }
150
+ }
151
+ return {
152
+ components: new Set([...expandedComponents].filter(component => availableComponents.has(component))),
153
+ explicitComponents: new Set([...explicitComponents].filter(component => availableComponents.has(component))),
154
+ scopedComponents
155
+ };
156
+ }
157
+ function shouldTransformElement(componentName, path, selection) {
158
+ if (selection.explicitComponents.has(componentName)) {
159
+ return true;
160
+ }
161
+ let allowedParents = selection.scopedComponents.get(componentName);
162
+ if (!allowedParents || allowedParents.size === 0) {
163
+ return true;
164
+ }
165
+ return !!path.findParent((parentPath) => t.isJSXElement(parentPath.node)
166
+ && t.isJSXIdentifier(parentPath.node.openingElement.name)
167
+ && allowedParents.has((0, utils_1.getName)(path, parentPath.node.openingElement.name)));
168
+ }
66
169
  function transformer(file, api, options) {
67
170
  let j = api.jscodeshift.withParser({
68
171
  parse(source) {
@@ -72,7 +175,8 @@ function transformer(file, api, options) {
72
175
  }
73
176
  });
74
177
  let root = j(file.source);
75
- let componentsToTransform = options.components ? new Set(options.components.split(',').filter(s => availableComponents.has(s))) : availableComponents;
178
+ let selection = getComponentSelection(options.components);
179
+ let componentsToTransform = selection.components;
76
180
  let v3ComponentsToRename = new Set(Object.keys(renamedComponents));
77
181
  let S2ComponentsToImport = new Set();
78
182
  let bindings = [];
@@ -80,8 +184,13 @@ function transformer(file, api, options) {
80
184
  let elements = [];
81
185
  let lastImportPath = null;
82
186
  let iconImports = new Map();
187
+ let illustrationImports = new Map();
188
+ let programPath = null;
83
189
  const leadingComments = root.find(j.Program).get('body', 0).node.leadingComments;
84
190
  (0, traverse_1.default)(root.paths()[0].node, {
191
+ Program(path) {
192
+ programPath = path;
193
+ },
85
194
  ImportDeclaration(path) {
86
195
  if (path.node.source.value === '@adobe/react-spectrum' || (path.node.source.value.startsWith('@react-spectrum/') && path.node.source.value !== '@react-spectrum/s2')) {
87
196
  lastImportPath = path;
@@ -97,7 +206,10 @@ function transformer(file, api, options) {
97
206
  if (propName && path.parentPath.parentPath?.parentPath?.isJSXElement()) {
98
207
  if (componentsToTransform.has(propName)) {
99
208
  importedComponents.set(propName, clonedSpecifier);
100
- elements.push([propName, path.parentPath.parentPath.parentPath]);
209
+ let elementPath = path.parentPath.parentPath.parentPath;
210
+ if (shouldTransformElement(propName, elementPath, selection)) {
211
+ elements.push([propName, elementPath]);
212
+ }
101
213
  }
102
214
  else if (v3ComponentsToRename.has(propName)) {
103
215
  S2ComponentsToImport.add(renamedComponents[propName]);
@@ -141,7 +253,10 @@ function transformer(file, api, options) {
141
253
  bindings.push(binding);
142
254
  for (let path of binding.referencePaths) {
143
255
  if (path.parentPath?.isJSXOpeningElement() && path.parentPath.parentPath.isJSXElement()) {
144
- elements.push([specifier.imported.name, path.parentPath.parentPath]);
256
+ let elementPath = path.parentPath.parentPath;
257
+ if (shouldTransformElement(specifier.imported.name, elementPath, selection)) {
258
+ elements.push([specifier.imported.name, elementPath]);
259
+ }
145
260
  }
146
261
  }
147
262
  }
@@ -167,6 +282,24 @@ function transformer(file, api, options) {
167
282
  iconImports.set(localName, { path, newName: null });
168
283
  }
169
284
  }
285
+ else if (path.node.source.value.startsWith('@spectrum-icons/illustrations/')) {
286
+ let illustrationName = path.node.source.value.split('/').pop();
287
+ if (!illustrationName) {
288
+ return;
289
+ }
290
+ let specifier = path.node.specifiers[0];
291
+ if (!specifier || !t.isImportDefaultSpecifier(specifier)) {
292
+ return;
293
+ }
294
+ let localName = specifier.local.name;
295
+ if (illustrationMap_1.illustrationMap.has(illustrationName)) {
296
+ let newIllustrationName = illustrationMap_1.illustrationMap.get(illustrationName);
297
+ illustrationImports.set(localName, { path, newName: newIllustrationName });
298
+ }
299
+ else {
300
+ illustrationImports.set(localName, { path, newName: null });
301
+ }
302
+ }
170
303
  },
171
304
  Import(path) {
172
305
  let call = path.parentPath;
@@ -177,7 +310,9 @@ function transformer(file, api, options) {
177
310
  if (arg.type !== 'StringLiteral') {
178
311
  return;
179
312
  }
180
- if (arg.value !== '@adobe/react-spectrum' && !arg.value.startsWith('@react-spectrum/')) {
313
+ let isV3ImportSource = arg.value === '@adobe/react-spectrum'
314
+ || (arg.value.startsWith('@react-spectrum/') && arg.value !== '@react-spectrum/s2');
315
+ if (!isV3ImportSource) {
181
316
  return;
182
317
  }
183
318
  // TODO: implement this. could be a bit challenging. punting for now.
@@ -191,6 +326,12 @@ function transformer(file, api, options) {
191
326
  (0, utils_1.addComment)(path.node, ` TODO(S2-upgrade): A Spectrum 2 equivalent to '${name.name}' was not found. Please update this icon manually.`);
192
327
  }
193
328
  }
329
+ if (t.isJSXIdentifier(name) && illustrationImports.has(name.name)) {
330
+ let illustrationInfo = illustrationImports.get(name.name);
331
+ if (illustrationInfo.newName === null) {
332
+ (0, utils_1.addComment)(path.node, ` TODO(S2-upgrade): A Spectrum 2 equivalent to '${name.name}' was not found. Please update this illustration manually.`);
333
+ }
334
+ }
194
335
  }
195
336
  });
196
337
  iconImports.forEach((iconInfo, localName) => {
@@ -216,6 +357,26 @@ function transformer(file, api, options) {
216
357
  path.node.specifiers = [t.importDefaultSpecifier(t.identifier(newLocalName))];
217
358
  }
218
359
  });
360
+ illustrationImports.forEach((illustrationInfo, localName) => {
361
+ let { path, newName } = illustrationInfo;
362
+ if (newName) {
363
+ let newImportSource = `@react-spectrum/s2/illustrations/linear/${newName}`;
364
+ let newLocalName = localName;
365
+ if (localName === path.node.source.value.split('/').pop() && localName !== newName) {
366
+ let binding = path.scope.getBinding(localName);
367
+ if (binding && !path.scope.hasBinding(newName)) {
368
+ newLocalName = newName;
369
+ binding.referencePaths.forEach(refPath => {
370
+ if (t.isJSXIdentifier(refPath.node)) {
371
+ refPath.node.name = newName;
372
+ }
373
+ });
374
+ }
375
+ }
376
+ path.node.source = t.stringLiteral(newImportSource);
377
+ path.node.specifiers = [t.importDefaultSpecifier(t.identifier(newLocalName))];
378
+ }
379
+ });
219
380
  let hasMacros = false;
220
381
  let usedLightDark = false;
221
382
  elements.forEach(([elementName, path]) => {
@@ -268,16 +429,24 @@ function transformer(file, api, options) {
268
429
  });
269
430
  if (existingImport.length) {
270
431
  let importDecl = existingImport.get();
271
- for (let specifier of importDecl.node.specifiers) {
272
- if (specifier.type === 'ImportSpecifier'
273
- && importedComponents.has(specifier.imported.name)) {
274
- importSpecifiers.add(specifier);
275
- }
276
- }
432
+ let existingSpecifiers = importDecl.value.specifiers;
277
433
  // add importSpecifiers to existing import
278
434
  importDecl.value.specifiers = [...importDecl.value.specifiers, ...[...importSpecifiers].filter(specifier => {
279
- // @ts-ignore
280
- return specifier.imported.name !== 'Item' && ![...importDecl.value.specifiers].find(s => s.imported.name === specifier.imported.name);
435
+ if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) {
436
+ if (specifier.imported.name === 'Item') {
437
+ return false;
438
+ }
439
+ let importedName = specifier.imported.name;
440
+ let localName = specifier.local?.name || importedName;
441
+ return !existingSpecifiers.find((s) => t.isImportSpecifier(s)
442
+ && t.isIdentifier(s.imported)
443
+ && s.imported.name === importedName
444
+ && (s.local?.name || s.imported.name) === localName);
445
+ }
446
+ if (t.isImportNamespaceSpecifier(specifier)) {
447
+ return !existingSpecifiers.find((s) => t.isImportNamespaceSpecifier(s) && s.local.name === specifier.local.name);
448
+ }
449
+ return false;
281
450
  })];
282
451
  }
283
452
  else {
@@ -305,6 +474,9 @@ function transformer(file, api, options) {
305
474
  }
306
475
  });
307
476
  }
477
+ if (programPath) {
478
+ (0, utils_1.removeUnusedImports)(programPath, ['@react-spectrum/s2']);
479
+ }
308
480
  root.find(j.Program).get('body', 0).node.comments = leadingComments;
309
481
  return root.toSource().replace(/assert\s*\{\s*type:\s*"macro"\s*\}/g, 'with { type: "macro" }');
310
482
  }
@@ -51,6 +51,7 @@ let availableComponents = (0, getComponents_1.getComponents)();
51
51
  * - Convert dynamic collections render function to items.map.
52
52
  */
53
53
  function transformActionGroup(path) {
54
+ let program = path.findParent((p) => t.isProgram(p.node));
54
55
  // Comment out overflowMode
55
56
  (0, transforms_1.commentOutProp)(path, { propName: 'overflowMode' });
56
57
  // Comment out buttonLabelBehavior
@@ -73,12 +74,10 @@ function transformActionGroup(path) {
73
74
  }
74
75
  let localName = newComponentName;
75
76
  if (availableComponents.has(newComponentName)) {
76
- let program = path.findParent((p) => t.isProgram(p.node));
77
77
  localName = (0, utils_1.addComponentImport)(program, newComponentName);
78
78
  }
79
79
  let localChildName = childComponentName;
80
80
  if (availableComponents.has(childComponentName)) {
81
- let program = path.findParent((p) => t.isProgram(p.node));
82
81
  localChildName = (0, utils_1.addComponentImport)(program, childComponentName);
83
82
  }
84
83
  // Convert dynamic collection to an array.map.
@@ -161,4 +160,5 @@ function transformActionGroup(path) {
161
160
  if (path.node.closingElement) {
162
161
  path.node.closingElement.name = t.jsxIdentifier(localName);
163
162
  }
163
+ (0, utils_1.removeComponentImportIfUnused)(program, 'Item');
164
164
  }
@@ -65,4 +65,5 @@ function transformContextualHelpTrigger(path) {
65
65
  }
66
66
  }
67
67
  }
68
+ (0, utils_1.removeComponentImportIfUnused)(program, 'Dialog');
68
69
  }
@@ -49,6 +49,7 @@ let availableComponents = (0, getComponents_1.getComponents)();
49
49
  * - When `type="fullscreenTakeover"`, replaces Dialog with `<FullscreenDialog variant="fullscreenTakeover">`.
50
50
  */
51
51
  function updateDialogChild(path) {
52
+ let program = path.findParent((p) => t.isProgram(p.node));
52
53
  let typePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'type');
53
54
  let type = typePath?.node.value?.type === 'StringLiteral' ? typePath.node.value?.value : 'modal';
54
55
  let newComponentName = 'Dialog';
@@ -72,7 +73,6 @@ function updateDialogChild(path) {
72
73
  typePath?.remove();
73
74
  let localName = newComponentName;
74
75
  if (newComponentName !== 'Dialog' && availableComponents.has(newComponentName)) {
75
- let program = path.findParent((p) => t.isProgram(p.node));
76
76
  localName = (0, utils_1.addComponentImport)(program, newComponentName);
77
77
  }
78
78
  path.traverse({
@@ -87,6 +87,20 @@ function updateDialogChild(path) {
87
87
  dialog.node.openingElement.attributes.push(...props);
88
88
  }
89
89
  });
90
+ path.traverse({
91
+ JSXElement(childPath) {
92
+ if (t.isJSXIdentifier(childPath.node.openingElement.name)
93
+ && (0, utils_1.getName)(childPath, childPath.node.openingElement.name) === 'Divider'
94
+ && t.isJSXElement(childPath.parentPath.node)
95
+ && t.isJSXIdentifier(childPath.parentPath.node.openingElement.name)) {
96
+ let parentName = (0, utils_1.getName)(childPath, childPath.parentPath.node.openingElement.name);
97
+ if (parentName === 'Dialog' || parentName === 'Popover' || parentName === 'FullscreenDialog') {
98
+ childPath.remove();
99
+ }
100
+ }
101
+ }
102
+ });
103
+ (0, utils_1.removeComponentImportIfUnused)(program, 'Divider');
90
104
  }
91
105
  /**
92
106
  * Transforms DialogTrigger:
@@ -38,11 +38,12 @@ const utils_1 = require("../../shared/utils");
38
38
  const transforms_1 = require("../../shared/transforms");
39
39
  const t = __importStar(require("@babel/types"));
40
40
  function transformTabList(tabListPath) {
41
- tabListPath.get('children').forEach(itemPath => {
42
- if (t.isJSXElement(itemPath.node) &&
43
- t.isJSXIdentifier(itemPath.node.openingElement.name) &&
44
- (0, utils_1.getName)(itemPath, itemPath.node.openingElement.name) === 'Item') {
45
- (0, transforms_1.updateComponentWithinCollection)(itemPath, { parentComponentName: 'TabList', newComponentName: 'Tab' });
41
+ tabListPath.traverse({
42
+ JSXElement(itemPath) {
43
+ if (t.isJSXIdentifier(itemPath.node.openingElement.name) &&
44
+ (0, utils_1.getName)(itemPath, itemPath.node.openingElement.name) === 'Item') {
45
+ (0, transforms_1.updateComponentWithinCollection)(itemPath, { parentComponentName: 'TabList', newComponentName: 'Tab' });
46
+ }
46
47
  }
47
48
  });
48
49
  return tabListPath.node;
@@ -113,4 +114,5 @@ function transformTabs(path) {
113
114
  (0, transforms_1.removeProp)(path, { propName: 'isEmphasized' });
114
115
  // Remove isQuiet
115
116
  (0, transforms_1.removeProp)(path, { propName: 'isQuiet' });
117
+ (0, utils_1.removeComponentImportIfUnused)(program, 'Item');
116
118
  }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.illustrationMap = void 0;
4
+ exports.illustrationMap = new Map([
5
+ ['Error', 'Error'],
6
+ ['File', 'Document'],
7
+ ['Folder', 'FolderClose'],
8
+ ['NoSearchResults', 'NoSearchResults'],
9
+ ['NotFound', 'NoSearchResults'],
10
+ ['Timeout', 'Clock'],
11
+ ['Unauthorized', 'LockClose'],
12
+ ['Unavailable', 'Error'],
13
+ ['Upload', 'Upload']
14
+ ]);
@@ -404,6 +404,7 @@ function movePropToNewChildComponentName(path, options) {
404
404
  (0, utils_1.getName)(path, path.node.openingElement.name) === childComponentName &&
405
405
  (0, utils_1.getName)(path, path.parentPath.node.openingElement.name) === parentComponentName) {
406
406
  let propValue;
407
+ let localName = newChildComponentName;
407
408
  path.node.openingElement.attributes =
408
409
  path.node.openingElement.attributes.filter((attr) => {
409
410
  if (t.isJSXAttribute(attr) && attr.name.name === propName) {
@@ -413,7 +414,11 @@ function movePropToNewChildComponentName(path, options) {
413
414
  return true;
414
415
  });
415
416
  if (propValue) {
416
- path.node.children.unshift(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(newChildComponentName), []), t.jsxClosingElement(t.jsxIdentifier(newChildComponentName)), [t.isStringLiteral(propValue) ? t.jsxText(propValue.value) : propValue]));
417
+ if (availableComponents.has(newChildComponentName)) {
418
+ let program = path.findParent((p) => t.isProgram(p.node));
419
+ localName = (0, utils_1.addComponentImport)(program, newChildComponentName);
420
+ }
421
+ path.node.children.unshift(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(localName), []), t.jsxClosingElement(t.jsxIdentifier(localName)), [t.isStringLiteral(propValue) ? t.jsxText(propValue.value) : propValue]));
417
422
  // TODO: handle dynamic collections. Need to wrap export function child in <Collection> and move `items` prop down.
418
423
  }
419
424
  }
@@ -39,6 +39,8 @@ exports.capitalize = capitalize;
39
39
  exports.addComment = addComment;
40
40
  exports.addComponentImport = addComponentImport;
41
41
  exports.removeComponentImport = removeComponentImport;
42
+ exports.removeComponentImportIfUnused = removeComponentImportIfUnused;
43
+ exports.removeUnusedImports = removeUnusedImports;
42
44
  exports.getName = getName;
43
45
  const t = __importStar(require("@babel/types"));
44
46
  function getPropValue(node) {
@@ -108,6 +110,17 @@ function addComment(node, comment) {
108
110
  });
109
111
  }
110
112
  function addComponentImport(path, newComponentName) {
113
+ let existingImport = path.node.body.find((node) => t.isImportDeclaration(node) && node.source.value === '@react-spectrum/s2');
114
+ if (existingImport && t.isImportDeclaration(existingImport)) {
115
+ let existingSpecifier = existingImport.specifiers.find((specifier) => {
116
+ return (t.isImportSpecifier(specifier) &&
117
+ specifier.imported.type === 'Identifier' &&
118
+ specifier.imported.name === newComponentName);
119
+ });
120
+ if (existingSpecifier && t.isImportSpecifier(existingSpecifier)) {
121
+ return existingSpecifier.local?.name ?? newComponentName;
122
+ }
123
+ }
111
124
  // If newComponentName variable already exists in scope, alias new import to avoid conflict.
112
125
  let existingBinding = path.scope.getBinding(newComponentName);
113
126
  let localName = newComponentName;
@@ -120,17 +133,7 @@ function addComponentImport(path, newComponentName) {
120
133
  }
121
134
  localName = newName;
122
135
  }
123
- let existingImport = path.node.body.find((node) => t.isImportDeclaration(node) && node.source.value === '@react-spectrum/s2');
124
136
  if (existingImport && t.isImportDeclaration(existingImport)) {
125
- let specifier = existingImport.specifiers.find((specifier) => {
126
- return (t.isImportSpecifier(specifier) &&
127
- specifier.imported.type === 'Identifier' &&
128
- specifier.imported.name === newComponentName);
129
- });
130
- if (specifier) {
131
- // Already imported
132
- return localName;
133
- }
134
137
  existingImport.specifiers.push(t.importSpecifier(t.identifier(localName), t.identifier(newComponentName)));
135
138
  }
136
139
  else {
@@ -142,18 +145,66 @@ function addComponentImport(path, newComponentName) {
142
145
  return localName;
143
146
  }
144
147
  function removeComponentImport(path, component) {
145
- let existingImport = path.node.body.find((node) => t.isImportDeclaration(node) && node.source.value === '@adobe/react-spectrum' || t.isImportDeclaration(node) && node.source.value.startsWith('@react-spectrum/'));
146
- if (existingImport && t.isImportDeclaration(existingImport)) {
147
- let specifier = existingImport.specifiers.find((specifier) => {
148
- return (t.isImportSpecifier(specifier) &&
149
- specifier.imported.type === 'Identifier' &&
150
- specifier.imported.name === component);
148
+ let imports = path.node.body.filter((node) => {
149
+ return t.isImportDeclaration(node)
150
+ && (node.source.value === '@adobe/react-spectrum'
151
+ || (node.source.value.startsWith('@react-spectrum/') && node.source.value !== '@react-spectrum/s2'));
152
+ });
153
+ for (let importDecl of imports) {
154
+ let previousLength = importDecl.specifiers.length;
155
+ importDecl.specifiers = importDecl.specifiers.filter((specifier) => {
156
+ return !(t.isImportSpecifier(specifier)
157
+ && specifier.imported.type === 'Identifier'
158
+ && specifier.imported.name === component);
159
+ });
160
+ if (importDecl.specifiers.length === 0 && previousLength > 0) {
161
+ path.node.body = path.node.body.filter((node) => node !== importDecl);
162
+ }
163
+ }
164
+ }
165
+ function removeComponentImportIfUnused(path, component) {
166
+ path.scope.crawl();
167
+ let imports = path.node.body.filter((node) => {
168
+ return t.isImportDeclaration(node)
169
+ && (node.source.value === '@adobe/react-spectrum'
170
+ || node.source.value.startsWith('@react-spectrum/'));
171
+ });
172
+ for (let importDecl of imports) {
173
+ let previousLength = importDecl.specifiers.length;
174
+ importDecl.specifiers = importDecl.specifiers.filter((specifier) => {
175
+ if (t.isImportSpecifier(specifier)
176
+ && specifier.imported.type === 'Identifier'
177
+ && specifier.imported.name === component) {
178
+ let localName = specifier.local?.name ?? specifier.imported.name;
179
+ let binding = path.scope.getBinding(localName);
180
+ return !!binding?.referencePaths.length;
181
+ }
182
+ return true;
151
183
  });
152
- if (specifier) {
153
- existingImport.specifiers = existingImport.specifiers.filter((s) => s !== specifier);
154
- if (existingImport.specifiers.length === 0) {
155
- path.node.body = path.node.body.filter((node) => node !== existingImport);
184
+ if (importDecl.specifiers.length === 0 && previousLength > 0) {
185
+ path.node.body = path.node.body.filter((node) => node !== importDecl);
186
+ }
187
+ }
188
+ }
189
+ function removeUnusedImports(path, sources) {
190
+ path.scope.crawl();
191
+ let sourceSet = new Set(sources);
192
+ let imports = path.node.body.filter((node) => {
193
+ return t.isImportDeclaration(node) && sourceSet.has(node.source.value);
194
+ });
195
+ for (let importDecl of imports) {
196
+ let previousLength = importDecl.specifiers.length;
197
+ importDecl.specifiers = importDecl.specifiers.filter((specifier) => {
198
+ if (t.isImportSpecifier(specifier)
199
+ || t.isImportDefaultSpecifier(specifier)
200
+ || t.isImportNamespaceSpecifier(specifier)) {
201
+ let binding = path.scope.getBinding(specifier.local.name);
202
+ return !!binding?.referencePaths.length;
156
203
  }
204
+ return true;
205
+ });
206
+ if (importDecl.specifiers.length === 0 && previousLength > 0) {
207
+ path.node.body = path.node.body.filter((node) => node !== importDecl);
157
208
  }
158
209
  }
159
210
  }
@@ -4,9 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getComponents = getComponents;
7
+ const fs_1 = require("fs");
7
8
  const parser_1 = require("@babel/parser");
8
9
  const path = require('path');
9
- const fs_1 = require("fs");
10
10
  const traverse_1 = __importDefault(require("@babel/traverse"));
11
11
  // These are exported but there are no codemods written for them yet.
12
12
  // Don't replace imports yet.
@@ -19,8 +19,18 @@ const skipped = new Set([
19
19
  function getComponents() {
20
20
  // Determine list of available components in S2 from index.ts
21
21
  let availableComponents = new Set();
22
- const packagePath = require.resolve('@react-spectrum/s2');
23
- const indexPath = path.join(path.dirname(packagePath), process.env.NODE_ENV === 'test' ? 'src/index.ts' : '../src/index.ts');
22
+ let indexPath;
23
+ try {
24
+ const packagePath = require.resolve('@react-spectrum/s2');
25
+ indexPath = path.join(path.dirname(packagePath), process.env.NODE_ENV === 'test' ? 'index.ts' : '../../exports/index.ts');
26
+ }
27
+ catch {
28
+ const workspaceIndexPath = path.resolve(__dirname, '../../../../../@react-spectrum/s2/exports/index.ts');
29
+ if (!(0, fs_1.existsSync)(workspaceIndexPath)) {
30
+ throw new Error('Could not resolve @react-spectrum/s2 source for codemods.');
31
+ }
32
+ indexPath = workspaceIndexPath;
33
+ }
24
34
  let index = (0, parser_1.parse)((0, fs_1.readFileSync)(indexPath, 'utf8'), { sourceType: 'module', plugins: ['typescript'] });
25
35
  (0, traverse_1.default)(index, {
26
36
  ExportNamedDeclaration(path) {
@@ -11,7 +11,24 @@ const logger_js_1 = __importDefault(require("./utils/logger.js"));
11
11
  const transform_js_1 = require("./transform.js");
12
12
  const waitForKeypress_js_1 = require("./utils/waitForKeypress.js");
13
13
  const boxen = require('boxen');
14
+ function printNextSteps(nextSteps) {
15
+ console.log(boxen(`Next steps:\n\n ${nextSteps.map((step, i) => `${i + 1}. ${step}`).join('\n\n\n')}`, { borderStyle: 'round', padding: 1, borderColor: 'green' }));
16
+ }
14
17
  async function s1_to_s2(options) {
18
+ if (options.agent) {
19
+ logger_js_1.default.info('Running s1-to-s2 in agent mode (non-interactive, transform-only).');
20
+ logger_js_1.default.info('Upgrading components...');
21
+ await (0, transform_js_1.transform)(options);
22
+ logger_js_1.default.success('Upgrade complete!');
23
+ printNextSteps([
24
+ `Ensure ${chalk_1.default.bold('@react-spectrum/s2')} is installed.`,
25
+ `If your bundler is not Parcel v2.12.0+, configure the Spectrum 2 style macro support. See: ${chalk_1.default.underline('https://react-spectrum.adobe.com/getting-started#framework-setup')}`,
26
+ `Add ${chalk_1.default.bold('import \'@react-spectrum/s2/page.css\';')} to your entry component if needed.`,
27
+ `Search for ${chalk_1.default.bold('TODO(S2-upgrade)')} and resolve remaining manual migration updates.`,
28
+ `Reference the migration guide: ${chalk_1.default.underline('https://react-spectrum.adobe.com/migrating')}`
29
+ ]);
30
+ return;
31
+ }
15
32
  console.log(boxen('Welcome to the React Spectrum v3 to Spectrum 2 upgrade assistant!\n\n' +
16
33
  'This tool will:\n\n' +
17
34
  `1. Install the ${chalk_1.default.bold('@react-spectrum/s2')} package and setup your bundler to use the Spectrum 2 style macro.\n\n` +
@@ -44,14 +61,14 @@ async function s1_to_s2(options) {
44
61
  ` - Vite: ${chalk_1.default.underline('https://github.com/adobe/react-spectrum/tree/main/examples/s2-vite-project')}\n` +
45
62
  ` - Rollup: ${chalk_1.default.underline('https://github.com/adobe/react-spectrum/tree/main/examples/s2-rollup-starter-app')}\n` +
46
63
  ` - ESBuild: ${chalk_1.default.underline('https://github.com/adobe/react-spectrum/tree/main/examples/s2-esbuild-starter-app')}\n\n` +
47
- `or view documentation here: ${chalk_1.default.underline('https://react-spectrum.adobe.com/s2/index.html?path=/docs/intro--docs#configuring-your-bundler')}`);
64
+ `or view documentation here: ${chalk_1.default.underline('https://react-spectrum.adobe.com/getting-started#framework-setup')}`);
48
65
  }
49
66
  nextSteps.push('Handle remaining upgrades and run your project\'s linter or formatter.\n\n' +
50
67
  'There may have been some upgrades that we couldn\'t handle automatically. We marked these with comments containing:\n\n' +
51
68
  `${chalk_1.default.bold('TODO(S2-upgrade)')}\n\n` +
52
69
  'You should be able to search your codebase and handle these manually. \n\n' +
53
70
  'We also recommend running your project\'s code formatter (i.e. Prettier, ESLint) after the upgrade process to clean up any extraneous formatting from the codemod.\n\n' +
54
- `For additional help, reference the Spectrum 2 Migration Guide: ${chalk_1.default.underline('https://react-spectrum.adobe.com/s2/index.html?path=/docs/migrating--docs')}`);
55
- console.log(boxen(`Next steps:\n\n ${nextSteps.map((step, i) => `${i + 1}. ${step}`).join('\n\n\n')}`, { borderStyle: 'round', padding: 1, borderColor: 'green' }));
71
+ `For additional help, reference the Spectrum 2 Migration Guide: ${chalk_1.default.underline('https://react-spectrum.adobe.com/migrating')}`);
72
+ printNextSteps(nextSteps);
56
73
  process.exit(0);
57
74
  }
@@ -8,6 +8,7 @@ const Runner_js_1 = require("jscodeshift/src/Runner.js");
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const transformPath = path_1.default.join(__dirname, 'codemods', 'codemod.js');
10
10
  async function transform(options) {
11
- let { path: filePath = '.', ...rest } = options;
12
- return await (0, Runner_js_1.run)(transformPath, [filePath], rest);
11
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
+ const { path: filePath = '.', agent, ...jscodeshiftOptions } = options;
13
+ return await (0, Runner_js_1.run)(transformPath, [filePath], jscodeshiftOptions);
13
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-spectrum/codemods",
3
- "version": "3.0.0-nightly-796738501-260310",
3
+ "version": "3.0.0-nightly-6a0d13c57-260322",
4
4
  "main": "dist/index.js",
5
5
  "source": "src/index.ts",
6
6
  "bin": "dist/index.js",
@@ -21,11 +21,12 @@
21
21
  "url": "https://github.com/adobe/react-spectrum"
22
22
  },
23
23
  "dependencies": {
24
+ "@adobe/react-spectrum": "3.0.0-nightly-6a0d13c57-260322",
24
25
  "@babel/parser": "^7.24.5",
25
26
  "@babel/traverse": "^7.24.5",
26
27
  "@babel/types": "^7.24.5",
27
- "@react-spectrum/s2": "3.0.0-nightly-796738501-260310",
28
- "@react-types/shared": "3.0.0-nightly-796738501-260310",
28
+ "@react-spectrum/s2": "3.0.0-nightly-6a0d13c57-260322",
29
+ "@react-types/shared": "3.0.0-nightly-6a0d13c57-260322",
29
30
  "@types/node": "^24",
30
31
  "boxen": "^5.1.2",
31
32
  "chalk": "^4.0.0",