@react-spectrum/codemods 1.1.0 → 1.2.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/dist/index.js CHANGED
@@ -4,9 +4,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const { parseArgs } = require('node:util');
5
5
  const src_1 = require("./s1-to-s2/src");
6
6
  const src_2 = require("./use-monopackages/src");
7
+ const src_3 = require("./use-subpaths/src");
7
8
  const codemods = {
8
9
  's1-to-s2': src_1.s1_to_s2,
9
- 'use-monopackages': src_2.use_monopackages
10
+ 'use-monopackages': src_2.use_monopackages,
11
+ 'use-subpaths': src_3.use_subpaths
10
12
  };
11
13
  // https://github.com/facebook/jscodeshift?tab=readme-ov-file#usage-cli
12
14
  const options = {
@@ -25,6 +27,9 @@ const options = {
25
27
  },
26
28
  'components': {
27
29
  type: 'string'
30
+ },
31
+ 'agent': {
32
+ type: 'boolean'
28
33
  }
29
34
  };
30
35
  const { values, positionals } = parseArgs({
@@ -35,21 +40,22 @@ if (positionals.length < 1) {
35
40
  console.error('Please specify a codemod to run. Available codemods: ', Object.keys(codemods).join(', '));
36
41
  process.exit(1);
37
42
  }
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({
43
+ async function main() {
44
+ const codemodName = positionals[0];
45
+ const codemodFunction = codemods[codemodName];
46
+ if (!codemodFunction) {
47
+ console.error(`Unknown codemod: ${codemodName}, available codemods: ${Object.keys(codemods).join(', ')}`);
48
+ process.exit(1);
49
+ }
50
+ await Promise.resolve(codemodFunction({
46
51
  parser: 'tsx',
47
52
  ignorePattern: '**/node_modules/**',
48
53
  path: '.',
54
+ extensions: 'js,jsx,mjs,cjs,ts,tsx',
49
55
  ...values
50
- });
56
+ }));
51
57
  }
52
- catch (error) {
58
+ main().catch((error) => {
53
59
  console.error(`Error running codemod: ${error}`);
54
60
  process.exit(1);
55
- }
61
+ });
@@ -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
  }
@@ -4,12 +4,12 @@ exports.default = transformActionMenu;
4
4
  const transforms_1 = require("../../shared/transforms");
5
5
  /**
6
6
  * Transforms ActionMenu:
7
- * - Comment out closeOnSelect (it has not been implemented yet).
7
+ * - Rename `closeOnSelect` to `shouldCloseOnSelect`.
8
8
  * - Comment out trigger (it has not been implemented yet).
9
9
  */
10
10
  function transformActionMenu(path) {
11
- // Comment out closeOnSelect
12
- (0, transforms_1.commentOutProp)(path, { propName: 'closeOnSelect' });
11
+ // Rename `closeOnSelect` to `shouldCloseOnSelect`
12
+ (0, transforms_1.updatePropName)(path, { oldPropName: 'closeOnSelect', newPropName: 'shouldCloseOnSelect' });
13
13
  // Comment out trigger
14
14
  (0, transforms_1.commentOutProp)(path, { propName: 'trigger' });
15
15
  }
@@ -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:
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = transformMenuTrigger;
4
+ const transforms_1 = require("../../shared/transforms");
5
+ /**
6
+ * Transforms MenuTrigger:
7
+ * - Rename `closeOnSelect` to `shouldCloseOnSelect` and move it to the child `Menu`.
8
+ */
9
+ function transformMenuTrigger(path) {
10
+ (0, transforms_1.updatePropName)(path, { oldPropName: 'closeOnSelect', newPropName: 'shouldCloseOnSelect' });
11
+ (0, transforms_1.movePropToChildComponent)(path, {
12
+ parentComponentName: 'MenuTrigger',
13
+ childComponentName: 'Menu',
14
+ propName: 'shouldCloseOnSelect'
15
+ });
16
+ }
@@ -9,6 +9,9 @@ const transforms_1 = require("../../shared/transforms");
9
9
  * - Change validationState="invalid" to isInvalid.
10
10
  * - Remove validationState="valid" (it is no longer supported in Spectrum 2).
11
11
  * - Replace isLoading with loadingState.
12
+ * - Rename onSelectionChange to onChange.
13
+ * - Rename selectedKey to value.
14
+ * - Rename defaultSelectedKey to defaultValue.
12
15
  */
13
16
  function transformPicker(path) {
14
17
  // Change menuWidth value from a DimensionValue to a pixel value
@@ -30,4 +33,19 @@ function transformPicker(path) {
30
33
  newPropName: 'loadingState',
31
34
  comment: 'Replace boolean passed to isLoading with appropriate loadingState.'
32
35
  });
36
+ // Rename onSelectionChange to onChange
37
+ (0, transforms_1.updatePropName)(path, {
38
+ oldPropName: 'onSelectionChange',
39
+ newPropName: 'onChange'
40
+ });
41
+ // Rename selectedKey to value
42
+ (0, transforms_1.updatePropName)(path, {
43
+ oldPropName: 'selectedKey',
44
+ newPropName: 'value'
45
+ });
46
+ // Rename defaultSelectedKey to defaultValue
47
+ (0, transforms_1.updatePropName)(path, {
48
+ oldPropName: 'defaultSelectedKey',
49
+ newPropName: 'defaultValue'
50
+ });
33
51
  }
@@ -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
+ ]);
@@ -45,6 +45,7 @@ exports.updateComponentWithinCollection = updateComponentWithinCollection;
45
45
  exports.commentIfParentCollectionNotDetected = commentIfParentCollectionNotDetected;
46
46
  exports.movePropToNewChildComponentName = movePropToNewChildComponentName;
47
47
  exports.movePropToParentComponent = movePropToParentComponent;
48
+ exports.movePropToChildComponent = movePropToChildComponent;
48
49
  exports.updateToNewComponentName = updateToNewComponentName;
49
50
  exports.convertDimensionValueToPx = convertDimensionValueToPx;
50
51
  exports.updatePlacementToSingleValue = updatePlacementToSingleValue;
@@ -404,6 +405,7 @@ function movePropToNewChildComponentName(path, options) {
404
405
  (0, utils_1.getName)(path, path.node.openingElement.name) === childComponentName &&
405
406
  (0, utils_1.getName)(path, path.parentPath.node.openingElement.name) === parentComponentName) {
406
407
  let propValue;
408
+ let localName = newChildComponentName;
407
409
  path.node.openingElement.attributes =
408
410
  path.node.openingElement.attributes.filter((attr) => {
409
411
  if (t.isJSXAttribute(attr) && attr.name.name === propName) {
@@ -413,7 +415,11 @@ function movePropToNewChildComponentName(path, options) {
413
415
  return true;
414
416
  });
415
417
  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]));
418
+ if (availableComponents.has(newChildComponentName)) {
419
+ let program = path.findParent((p) => t.isProgram(p.node));
420
+ localName = (0, utils_1.addComponentImport)(program, newChildComponentName);
421
+ }
422
+ 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
423
  // TODO: handle dynamic collections. Need to wrap export function child in <Collection> and move `items` prop down.
418
424
  }
419
425
  }
@@ -440,6 +446,35 @@ function movePropToParentComponent(path, options) {
440
446
  }
441
447
  });
442
448
  }
449
+ /**
450
+ * Moves a prop from the parent component onto a direct child JSX element.
451
+ *
452
+ * Example:
453
+ * - MenuTrigger: Remove `shouldCloseOnSelect` and add it to the child `Menu` instead.
454
+ */
455
+ function movePropToChildComponent(path, options) {
456
+ const { parentComponentName, childComponentName, propName } = options;
457
+ if (!t.isJSXIdentifier(path.node.openingElement.name) ||
458
+ (0, utils_1.getName)(path, path.node.openingElement.name) !== parentComponentName) {
459
+ return;
460
+ }
461
+ let attrs = path.node.openingElement.attributes;
462
+ let propAttr = attrs.find((attr) => t.isJSXAttribute(attr) && attr.name.name === propName);
463
+ if (!propAttr) {
464
+ return;
465
+ }
466
+ let childPath = path.get('children').find((child) => child.isJSXElement() &&
467
+ t.isJSXIdentifier(child.node.openingElement.name) &&
468
+ (0, utils_1.getName)(path, child.node.openingElement.name) === childComponentName);
469
+ if (!childPath?.isJSXElement()) {
470
+ return;
471
+ }
472
+ childPath.node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier(propName), propAttr.value));
473
+ let index = attrs.indexOf(propAttr);
474
+ if (index !== -1) {
475
+ attrs.splice(index, 1);
476
+ }
477
+ }
443
478
  /**
444
479
  * Update to use a new component.
445
480
  *
@@ -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
  }
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const Module = require('module');
6
+ const url = require('url');
5
7
  function areSpecifiersAlphabetized(specifiers) {
6
8
  const specifierNames = specifiers.map((specifier) => specifier.imported.name);
7
9
  const sortedNames = [...specifierNames].sort();
@@ -41,7 +43,17 @@ const transformer = function transformer(file, api, options) {
41
43
  let anyIndexFound = false;
42
44
  const monopackageExports = {};
43
45
  selectedPackages.forEach((pkg) => {
44
- const indexPath = path.join(process.cwd(), `node_modules/${packages[pkg].monopackage}/dist/types.d.ts`);
46
+ let indexPath;
47
+ try {
48
+ let pkgPath = path.dirname(Module.findPackageJSON(packages[pkg].monopackage, url.pathToFileURL(file.path || `${process.cwd()}/index`)));
49
+ indexPath = `${pkgPath}/dist/types/exports/index.d.ts`;
50
+ if (!fs.existsSync(indexPath)) {
51
+ indexPath = `${pkgPath}/exports/index.ts`;
52
+ }
53
+ }
54
+ catch {
55
+ return;
56
+ }
45
57
  if (fs.existsSync(indexPath)) {
46
58
  anyIndexFound = true;
47
59
  const indexFile = fs.readFileSync(indexPath, 'utf8');
@@ -79,7 +91,7 @@ const transformer = function transformer(file, api, options) {
79
91
  const individualPackageImports = root
80
92
  .find(j.ImportDeclaration)
81
93
  .filter((path) => {
82
- return path.node.source.value?.startsWith(packages[pkg].individualPrefix);
94
+ return path.node.source.value !== '@react-spectrum/s2' && path.node.source.value?.startsWith(packages[pkg].individualPrefix);
83
95
  });
84
96
  if (individualPackageImports.size() === 0) {
85
97
  return;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = transformer;
37
+ const specifiers_1 = require("./specifiers");
38
+ const parser_1 = require("@babel/parser");
39
+ const recast_1 = require("recast");
40
+ const t = __importStar(require("@babel/types"));
41
+ function getImportedName(specifier) {
42
+ return specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value;
43
+ }
44
+ function getImportKey(specifier) {
45
+ const importedName = getImportedName(specifier);
46
+ const localName = specifier.local?.name ?? importedName;
47
+ const importKind = specifier.importKind ?? 'value';
48
+ return `${importKind}:${importedName}:${localName}`;
49
+ }
50
+ function getImportDeclarationKey(node) {
51
+ return (node.importKind ?? 'value') + ':' + node.source.value;
52
+ }
53
+ function getNamedSpecifierKeys(importDeclaration) {
54
+ let keys = new Set();
55
+ for (let specifier of importDeclaration.specifiers ?? []) {
56
+ if (specifier.type === 'ImportSpecifier') {
57
+ keys.add(getImportKey(specifier));
58
+ }
59
+ }
60
+ return keys;
61
+ }
62
+ function createImportDeclaration(source, importKind, specifiers) {
63
+ let declaration = t.importDeclaration(specifiers, t.stringLiteral(source));
64
+ declaration.importKind = importKind ?? 'value';
65
+ return declaration;
66
+ }
67
+ function resolveTargetSource(candidates, uniqueSources, existingImports) {
68
+ // Group with the first unique source that contained a candidate.
69
+ // For example if you imported ListBox, ActionGroup, and Item, Item would be grouped with ListBox.
70
+ for (let source of uniqueSources) {
71
+ if (candidates.includes(source)) {
72
+ return source;
73
+ }
74
+ }
75
+ // Group with an already existing import.
76
+ for (let source of existingImports.keys()) {
77
+ if (candidates.includes(source)) {
78
+ return source;
79
+ }
80
+ }
81
+ return candidates[0];
82
+ }
83
+ function transformer(file, api) {
84
+ let specifiersByPackage = (0, specifiers_1.getSpecifiersByPackage)(file.path);
85
+ let j = api.jscodeshift.withParser({
86
+ parse(source) {
87
+ return (0, recast_1.parse)(source, {
88
+ parser: {
89
+ parse(innerSource) {
90
+ return (0, parser_1.parse)(innerSource, {
91
+ sourceType: 'module',
92
+ plugins: [
93
+ 'jsx',
94
+ 'typescript',
95
+ 'importAssertions',
96
+ 'dynamicImport',
97
+ 'decorators-legacy',
98
+ 'classProperties',
99
+ 'classPrivateProperties',
100
+ 'classPrivateMethods',
101
+ 'exportDefaultFrom',
102
+ 'exportNamespaceFrom',
103
+ 'objectRestSpread',
104
+ 'optionalChaining',
105
+ 'nullishCoalescingOperator',
106
+ 'topLevelAwait'
107
+ ],
108
+ tokens: true,
109
+ errorRecovery: true
110
+ });
111
+ }
112
+ }
113
+ });
114
+ }
115
+ });
116
+ let root = j(file.source);
117
+ let program = root.get().node.program;
118
+ let uniqueSources = new Set();
119
+ let existingImports = new Map();
120
+ for (let node of program.body) {
121
+ if (node.type === 'ImportDeclaration') {
122
+ let source = node.source.value;
123
+ if (typeof source !== 'string') {
124
+ continue;
125
+ }
126
+ existingImports.set(getImportDeclarationKey(node), node);
127
+ if (source in specifiersByPackage) {
128
+ let sourceMap = specifiersByPackage[source];
129
+ for (let specifier of node.specifiers ?? []) {
130
+ if (specifier.type !== 'ImportSpecifier') {
131
+ continue;
132
+ }
133
+ let importedName = getImportedName(specifier);
134
+ let candidates = sourceMap[importedName];
135
+ if (candidates && (candidates.length === 1 || candidates[0] === `${source}/${importedName}`)) {
136
+ let importKind = node.importKind || 'value';
137
+ uniqueSources.add(importKind + ':' + candidates[0]);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ let didChange = false;
144
+ program.body = program.body.flatMap(node => {
145
+ if (node.type !== 'ImportDeclaration') {
146
+ return [node];
147
+ }
148
+ let source = node.source.value;
149
+ if (typeof source !== 'string' || !(source in specifiersByPackage)) {
150
+ return [node];
151
+ }
152
+ let importDeclaration = node;
153
+ let sourceMap = specifiersByPackage[node.source.value];
154
+ let movedSpecifiersBySource = new Map();
155
+ importDeclaration.specifiers = importDeclaration.specifiers.filter(specifier => {
156
+ if (specifier.type !== 'ImportSpecifier') {
157
+ return true;
158
+ }
159
+ let importedName = getImportedName(specifier);
160
+ let candidates = sourceMap[importedName];
161
+ if (!candidates || candidates.length === 0) {
162
+ return true;
163
+ }
164
+ let importKind = node.importKind || 'value';
165
+ let targetSource = candidates.length === 1
166
+ ? importKind + ':' + candidates[0]
167
+ : resolveTargetSource(candidates.map(c => importKind + ':' + c), uniqueSources, existingImports);
168
+ let movedSpecifiers = movedSpecifiersBySource.get(targetSource) ?? [];
169
+ movedSpecifiers.push(t.cloneNode(specifier, true));
170
+ movedSpecifiersBySource.set(targetSource, movedSpecifiers);
171
+ didChange = true;
172
+ return false;
173
+ });
174
+ if (movedSpecifiersBySource.size === 0) {
175
+ return [node];
176
+ }
177
+ let newDeclarations = [];
178
+ if (importDeclaration.specifiers.length > 0) {
179
+ newDeclarations.push(node);
180
+ }
181
+ for (let [targetSource, movedSpecifiers] of movedSpecifiersBySource) {
182
+ let destinationImport = existingImports.get(targetSource);
183
+ if (!destinationImport) {
184
+ destinationImport = createImportDeclaration(targetSource.slice(targetSource.indexOf(':') + 1), importDeclaration.importKind, []);
185
+ newDeclarations.push(destinationImport);
186
+ existingImports.set(targetSource, destinationImport);
187
+ }
188
+ let existingSpecifierKeys = getNamedSpecifierKeys(destinationImport);
189
+ for (let movedSpecifier of movedSpecifiers) {
190
+ let key = getImportKey(movedSpecifier);
191
+ if (!existingSpecifierKeys.has(key)) {
192
+ destinationImport.specifiers.push(movedSpecifier);
193
+ existingSpecifierKeys.add(key);
194
+ }
195
+ }
196
+ }
197
+ return newDeclarations;
198
+ });
199
+ return didChange ? root.toSource({ quote: 'single' }) : file.source;
200
+ }
201
+ ;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.use_subpaths = use_subpaths;
7
+ const Runner_js_1 = require("jscodeshift/src/Runner.js");
8
+ const path_1 = __importDefault(require("path"));
9
+ const transformPath = path_1.default.join(__dirname, 'codemod.js');
10
+ async function use_subpaths(options) {
11
+ let { path: filePath = '.', ...rest } = options;
12
+ return await (0, Runner_js_1.run)(transformPath, [filePath], rest);
13
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getSpecifiersByPackage = getSpecifiersByPackage;
7
+ /* eslint-disable max-depth */
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const module_1 = __importDefault(require("module"));
10
+ const parser_1 = require("@babel/parser");
11
+ const path_1 = __importDefault(require("path"));
12
+ const url_1 = __importDefault(require("url"));
13
+ const PACKAGES = [
14
+ '@adobe/react-spectrum',
15
+ '@react-spectrum/s2',
16
+ 'react-aria-components',
17
+ 'react-aria',
18
+ 'react-stately'
19
+ ];
20
+ const specifiersByPackage = {};
21
+ /** Builds a mapping of monopackage -> export -> subpaths that contain the export. */
22
+ function getSpecifiersByPackage(from) {
23
+ for (let pkg of PACKAGES) {
24
+ if (specifiersByPackage[pkg]) {
25
+ continue;
26
+ }
27
+ let dir;
28
+ try {
29
+ let pkgPath = path_1.default.dirname(module_1.default.findPackageJSON(pkg, url_1.default.pathToFileURL(from || `${process.cwd()}/index`)));
30
+ dir = `${pkgPath}/dist/types/exports`;
31
+ if (!fs_1.default.existsSync(dir)) {
32
+ dir = `${pkgPath}/exports`;
33
+ }
34
+ if (!fs_1.default.existsSync(dir)) {
35
+ continue;
36
+ }
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ let exports = {};
42
+ for (let entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
43
+ if (entry.name === 'index.ts' || entry.name === 'index.d.ts' || !entry.isFile()) {
44
+ continue;
45
+ }
46
+ let contents = fs_1.default.readFileSync(`${dir}/${entry.name}`, 'utf8');
47
+ let ast = (0, parser_1.parse)(contents, {
48
+ sourceType: 'module',
49
+ plugins: ['typescript']
50
+ });
51
+ let subpath = entry.name.replace(/(\.d)?\.ts$/, '');
52
+ let importSpecifier = `${pkg}/${subpath}`;
53
+ for (let node of ast.program.body) {
54
+ if (node.type === 'ExportNamedDeclaration') {
55
+ for (let specifier of node.specifiers) {
56
+ if (specifier.type !== 'ExportSpecifier') {
57
+ continue;
58
+ }
59
+ let exported = specifier.exported.type === 'Identifier' ? specifier.exported.name : specifier.exported.value;
60
+ exports[exported] ?? (exports[exported] = []);
61
+ if (exported.startsWith(subpath)) {
62
+ exports[exported].unshift(importSpecifier);
63
+ }
64
+ else {
65
+ exports[exported].push(importSpecifier);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ specifiersByPackage[pkg] = exports;
71
+ }
72
+ }
73
+ return specifiersByPackage;
74
+ }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@react-spectrum/codemods",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "source": "src/index.ts",
6
6
  "bin": "dist/index.js",
7
7
  "targets": {
8
8
  "main": false
9
9
  },
10
+ "engines": {
11
+ "node": ">=22.14.0"
12
+ },
10
13
  "scripts": {
11
14
  "build": "tsc",
12
15
  "prepublishOnly": "yarn build"
@@ -21,11 +24,12 @@
21
24
  "url": "https://github.com/adobe/react-spectrum"
22
25
  },
23
26
  "dependencies": {
27
+ "@adobe/react-spectrum": "3.47.0",
24
28
  "@babel/parser": "^7.24.5",
25
29
  "@babel/traverse": "^7.24.5",
26
30
  "@babel/types": "^7.24.5",
27
- "@react-spectrum/s2": "^1.2.0",
28
- "@react-types/shared": "^3.33.1",
31
+ "@react-spectrum/s2": "1.3.0",
32
+ "@react-types/shared": "^3.34.0",
29
33
  "@types/node": "^24",
30
34
  "boxen": "^5.1.2",
31
35
  "chalk": "^4.0.0",
@@ -49,5 +53,5 @@
49
53
  "publishConfig": {
50
54
  "access": "public"
51
55
  },
52
- "gitHead": "8df187370053aa35f553cb388ad670f65e1ab371"
56
+ "gitHead": "a6999bdf494a2e9c0381a5881908328bdd22ddae"
53
57
  }