Skip to content

Commit 7da3d38

Browse files
authored
Add codemod to replace individual package imports with monopackage imports (#6471)
* add codemod to replace individual package imports with monopackage imports * move file * remove extra file
1 parent 8577c4d commit 7da3d38

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
function areSpecifiersAlphabetized(specifiers) {
5+
const specifierNames = specifiers.map(
6+
(specifier) => specifier.imported.name
7+
);
8+
const sortedNames = [...specifierNames].sort();
9+
return specifierNames.join() === sortedNames.join();
10+
}
11+
12+
/**
13+
* Replaces individual package imports with monopackage imports, where possible.
14+
*
15+
* Works for:
16+
* - `@react-spectrum/*` -> `@adobe/react-spectrum`.
17+
* - `@react-aria/*` -> `react-aria`.
18+
* - `@react-stately/*` -> `react-stately`.
19+
*
20+
* By default this will apply to all the above packages, or optionally you can specify which packages to apply this by passing a comma-separated list to the packages option: `--packages=react-aria,react-stately,react-spectrum`.
21+
*
22+
* Run this from a directory where the relevant packages are installed in node_modules so it knows which monopackage exports are available to use (since exports may vary by version).
23+
*
24+
* 1. Install jscodeshift: `npm i -g jscodeshift`
25+
* 2. Run: `jscodeshift -t /path/to/use-monopackages.ts src/`.
26+
*/
27+
module.exports = function transformer(file, api, options) {
28+
const j = api.jscodeshift;
29+
const root = j(file.source);
30+
31+
const packages = {
32+
'react-spectrum': {
33+
monopackage: '@adobe/react-spectrum',
34+
individualPrefix: '@react-spectrum/'
35+
},
36+
'react-aria': {
37+
monopackage: 'react-aria',
38+
individualPrefix: '@react-aria/'
39+
},
40+
'react-stately': {
41+
monopackage: 'react-stately',
42+
individualPrefix: '@react-stately/'
43+
}
44+
};
45+
46+
const selectedPackages =
47+
options?.packages?.split(',').filter((pkg) => packages[pkg]) ||
48+
Object.keys(packages);
49+
50+
let anyIndexFound = false;
51+
const monopackageExports = {};
52+
53+
selectedPackages.forEach((pkg) => {
54+
const indexPath = path.join(
55+
process.cwd(),
56+
`node_modules/${packages[pkg].monopackage}/dist/types.d.ts`
57+
);
58+
59+
if (fs.existsSync(indexPath)) {
60+
anyIndexFound = true;
61+
const indexFile = fs.readFileSync(indexPath, 'utf8');
62+
const indexRoot = j(indexFile);
63+
64+
monopackageExports[pkg] = [];
65+
// Collect all named exports from the monopackage index file
66+
indexRoot.find(j.ExportNamedDeclaration).forEach((path) => {
67+
path.node.specifiers.forEach((specifier) => {
68+
monopackageExports[pkg].push(specifier.exported.name);
69+
});
70+
});
71+
72+
// Collect all exports defined in export statements like "export { Component } from '...' "
73+
indexRoot
74+
.find(j.ExportNamedDeclaration, {
75+
source: {
76+
type: 'Literal'
77+
}
78+
})
79+
.forEach((path) => {
80+
path.node.specifiers.forEach((specifier) => {
81+
monopackageExports[pkg].push(specifier.exported.name);
82+
});
83+
});
84+
} else if (options?.packages?.split(',').includes(pkg)) {
85+
console.warn(
86+
`The index file for ${packages[pkg].monopackage} at ${indexPath} does not exist. Ensure that ${packages[pkg].monopackage} is installed.`
87+
);
88+
}
89+
});
90+
91+
if (!anyIndexFound) {
92+
console.warn(
93+
'None of the index files for the selected packages exist. Ensure that the packages are installed.'
94+
);
95+
return root.toSource();
96+
}
97+
98+
selectedPackages.forEach((pkg) => {
99+
// Find all imports from individual packages
100+
const individualPackageImports = root
101+
.find(j.ImportDeclaration)
102+
.filter((path) => {
103+
return path.node.source.value.startsWith(
104+
packages[pkg].individualPrefix
105+
);
106+
});
107+
108+
if (individualPackageImports.size() === 0) {
109+
return;
110+
}
111+
112+
// Collect all imported specifiers from individual packages that are also in the monopackage exports
113+
const importedSpecifiers = individualPackageImports
114+
.nodes()
115+
.reduce((acc, node) => {
116+
node.specifiers.forEach((specifier) => {
117+
if (monopackageExports[pkg].includes(specifier.imported.name)) {
118+
acc.push(specifier);
119+
}
120+
});
121+
return acc;
122+
}, []);
123+
124+
// Remove the old imports if they are present in monopackage exports
125+
individualPackageImports.forEach((path) => {
126+
path.node.specifiers = path.node.specifiers.filter(
127+
(specifier) =>
128+
!monopackageExports[pkg].includes(specifier.imported.name)
129+
);
130+
});
131+
132+
// Remove import declarations with no specifiers left
133+
individualPackageImports
134+
.filter((path) => path.node.specifiers.length === 0)
135+
.remove();
136+
137+
// Find existing monopackage import if it exists
138+
const monopackageImport = root.find(j.ImportDeclaration).filter((path) => {
139+
return path.node.source.value === packages[pkg].monopackage;
140+
});
141+
142+
if (monopackageImport.size() > 0) {
143+
const existingImport = monopackageImport.at(0).get().node;
144+
const specifiers = existingImport.specifiers;
145+
if (areSpecifiersAlphabetized(specifiers)) {
146+
// If imports are sorted, add the new import in sorted order
147+
importedSpecifiers.forEach((newSpecifier) => {
148+
const index = specifiers.findIndex(
149+
(specifier) => specifier.imported.name > newSpecifier.imported.name
150+
);
151+
if (index === -1) {
152+
specifiers.push(newSpecifier);
153+
} else {
154+
specifiers.splice(index, 0, newSpecifier);
155+
}
156+
});
157+
} else {
158+
// If imports are not sorted, add the new import to the end
159+
specifiers.push(...importedSpecifiers);
160+
}
161+
} else if (importedSpecifiers.length > 0) {
162+
// Create a new monopackage import with the collected specifiers
163+
const newImport = j.importDeclaration(
164+
importedSpecifiers,
165+
j.literal(packages[pkg].monopackage)
166+
);
167+
168+
// Insert the new import below the last existing import
169+
const lastImport = root.find(j.ImportDeclaration).at(-1);
170+
if (lastImport.size() > 0) {
171+
lastImport.insertAfter(newImport);
172+
} else {
173+
root.get().node.program.body.unshift(newImport);
174+
}
175+
}
176+
});
177+
178+
return root.toSource();
179+
};
180+
181+
module.exports.parser = 'tsx';

0 commit comments

Comments
 (0)