ast-utils.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. const validateIdentifier = require("./validate-identifier");
  2. function safeTraverse(obj, paths) {
  3. let val = obj;
  4. let idx = 0;
  5. while (idx < paths.length) {
  6. if (!val) {
  7. return null;
  8. }
  9. val = val[paths[idx]];
  10. idx++;
  11. }
  12. return val;
  13. }
  14. // Convert nested MemberExpressions to strings like webpack.optimize.DedupePlugin
  15. function memberExpressionToPathString(path) {
  16. if (path && path.object) {
  17. return [memberExpressionToPathString(path.object), path.property.name].join(
  18. "."
  19. );
  20. }
  21. return path.name;
  22. }
  23. // Convert Array<string> like ['webpack', 'optimize', 'DedupePlugin'] to nested MemberExpressions
  24. function pathsToMemberExpression(j, paths) {
  25. if (!paths.length) {
  26. return null;
  27. } else if (paths.length === 1) {
  28. return j.identifier(paths[0]);
  29. } else {
  30. const first = paths.slice(0, 1);
  31. const rest = paths.slice(1);
  32. return j.memberExpression(
  33. pathsToMemberExpression(j, rest),
  34. pathsToMemberExpression(j, first)
  35. );
  36. }
  37. }
  38. /**
  39. *
  40. * Find paths that match `new name.space.PluginName()` for a
  41. * given array of plugin names
  42. *
  43. * @param {any} j — jscodeshift API
  44. * @param {Node} node - Node to start search from
  45. * @param {String[]} pluginNamesArray - Array of plugin names like `webpack.LoaderOptionsPlugin`
  46. * @returns {Node} Node that has the pluginName
  47. */
  48. function findPluginsByName(j, node, pluginNamesArray) {
  49. return node.find(j.NewExpression).filter(path => {
  50. return pluginNamesArray.some(
  51. plugin =>
  52. memberExpressionToPathString(path.get("callee").value) === plugin
  53. );
  54. });
  55. }
  56. /**
  57. * It lookouts for the plugins property and, if the array is empty, it removes it from the AST
  58. * @param {any} j - jscodeshift API
  59. * @param {Node} rootNode - node to start search from
  60. * @returns {Node} rootNode modified AST.
  61. */
  62. function findPluginsArrayAndRemoveIfEmpty(j, rootNode) {
  63. return rootNode.find(j.Identifier, { name: "plugins" }).forEach(node => {
  64. const elements = safeTraverse(node, [
  65. "parent",
  66. "value",
  67. "value",
  68. "elements"
  69. ]);
  70. if (!elements.length) {
  71. j(node.parent).remove();
  72. }
  73. });
  74. }
  75. /**
  76. *
  77. * Finds the path to the `name: []` node
  78. *
  79. * @param {any} j — jscodeshift API
  80. * @param {Node} node - Node to start search from
  81. * @param {String} propName - property to search for
  82. * @returns {Node} found node and
  83. */
  84. function findRootNodesByName(j, node, propName) {
  85. return node.find(j.Property, { key: { name: propName } });
  86. }
  87. /**
  88. *
  89. * Creates an Object's property with a given key and value
  90. *
  91. * @param {any} j — jscodeshift API
  92. * @param {String | Number} key - Property key
  93. * @param {String | Number | Boolean} value - Property value
  94. * @returns {Node}
  95. */
  96. function createProperty(j, key, value) {
  97. return j.property(
  98. "init",
  99. createIdentifierOrLiteral(j, key),
  100. createLiteral(j, value)
  101. );
  102. }
  103. /**
  104. *
  105. * Creates an appropriate literal property
  106. *
  107. * @param {any} j — jscodeshift API
  108. * @param {String | Boolean | Number} val
  109. * @returns {Node}
  110. */
  111. function createLiteral(j, val) {
  112. let literalVal = val;
  113. // We'll need String to native type conversions
  114. if (typeof val === "string") {
  115. // 'true' => true
  116. if (val === "true") literalVal = true;
  117. // 'false' => false
  118. if (val === "false") literalVal = false;
  119. // '1' => 1
  120. if (!isNaN(Number(val))) literalVal = Number(val);
  121. }
  122. return j.literal(literalVal);
  123. }
  124. /**
  125. *
  126. * Creates an appropriate identifier or literal property
  127. *
  128. * @param {any} j — jscodeshift API
  129. * @param {String | Boolean | Number} val
  130. * @returns {Node}
  131. */
  132. function createIdentifierOrLiteral(j, val) {
  133. // IPath<IIdentifier> | IPath<ILiteral> doesn't work, find another way
  134. let literalVal = val;
  135. // We'll need String to native type conversions
  136. if (typeof val === "string" || val.__paths) {
  137. // 'true' => true
  138. if (val === "true") {
  139. literalVal = true;
  140. return j.literal(literalVal);
  141. }
  142. // 'false' => false
  143. if (val === "false") {
  144. literalVal = false;
  145. return j.literal(literalVal);
  146. }
  147. // '1' => 1
  148. if (!isNaN(Number(val))) {
  149. literalVal = Number(val);
  150. return j.literal(literalVal);
  151. }
  152. if (val.__paths) {
  153. let regExpVal = val.__paths[0].value.program.body[0].expression;
  154. return j.literal(regExpVal.value);
  155. } else {
  156. // Use identifier instead
  157. if (
  158. !validateIdentifier.isKeyword(literalVal) ||
  159. !validateIdentifier.isIdentifierStart(literalVal) ||
  160. !validateIdentifier.isIdentifierChar(literalVal)
  161. )
  162. return j.identifier(literalVal);
  163. }
  164. }
  165. return j.literal(literalVal);
  166. }
  167. /**
  168. *
  169. * Adds or updates the value of a key within a root
  170. * webpack configuration property that's of type Object.
  171. *
  172. * @param {any} j — jscodeshift API
  173. * @param {Node} rootNode - node of root webpack configuration
  174. * @param {String} configProperty - key of an Object webpack configuration property
  175. * @param {String} key - key within the configuration object to update
  176. * @param {Object} value - the value to set for the key
  177. * @returns {Void}
  178. */
  179. function addOrUpdateConfigObject(j, rootNode, configProperty, key, value) {
  180. const propertyExists = rootNode.properties.filter(
  181. node => node.key.name === configProperty
  182. ).length;
  183. if (propertyExists) {
  184. rootNode.properties
  185. .filter(path => path.key.name === configProperty)
  186. .forEach(path => {
  187. const newProperties = path.value.properties.filter(
  188. path => path.key.name !== key
  189. );
  190. newProperties.push(j.objectProperty(j.identifier(key), value));
  191. path.value.properties = newProperties;
  192. });
  193. } else {
  194. rootNode.properties.push(
  195. j.objectProperty(
  196. j.identifier(configProperty),
  197. j.objectExpression([j.objectProperty(j.identifier(key), value)])
  198. )
  199. );
  200. }
  201. }
  202. /**
  203. *
  204. * Finds and removes a node for a given plugin name. If the plugin
  205. * is the last in the plugins array, the array is also removed.
  206. *
  207. * @param {any} j — jscodeshift API
  208. * @param {Node} node - node to start search from
  209. * @param {String} pluginName - name of the plugin to remove
  210. * @returns {Node | Void} - path to the root webpack configuration object if plugin is found
  211. */
  212. function findAndRemovePluginByName(j, node, pluginName) {
  213. let rootPath;
  214. findPluginsByName(j, node, [pluginName])
  215. .filter(path => safeTraverse(path, ["parent", "value"]))
  216. .forEach(path => {
  217. rootPath = safeTraverse(path, ["parent", "parent", "parent", "value"]);
  218. const arrayPath = path.parent.value;
  219. if (arrayPath.elements && arrayPath.elements.length === 1) {
  220. j(path.parent.parent).remove();
  221. } else {
  222. j(path).remove();
  223. }
  224. });
  225. return rootPath;
  226. }
  227. /**
  228. *
  229. * Finds or creates a node for a given plugin name string with options object
  230. * If plugin declaration already exist, options are merged.
  231. *
  232. * @param {any} j — jscodeshift API
  233. * @param {Node} rootNodePath - `plugins: []` NodePath where plugin should be added. See https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation#nodepaths
  234. * @param {String} pluginName - ex. `webpack.LoaderOptionsPlugin`
  235. * @param {Object} options - plugin options
  236. * @returns {Void}
  237. */
  238. function createOrUpdatePluginByName(j, rootNodePath, pluginName, options) {
  239. const pluginInstancePath = findPluginsByName(j, j(rootNodePath), [
  240. pluginName
  241. ]);
  242. let optionsProps;
  243. if (options) {
  244. optionsProps = Object.keys(options).map(key => {
  245. return createProperty(j, key, options[key]);
  246. });
  247. }
  248. // If plugin declaration already exist
  249. if (pluginInstancePath.size()) {
  250. pluginInstancePath.forEach(path => {
  251. // There are options we want to pass as argument
  252. if (optionsProps) {
  253. const args = path.value.arguments;
  254. if (args.length) {
  255. // Plugin is called with object as arguments
  256. // we will merge those objects
  257. let currentProps = j(path)
  258. .find(j.ObjectExpression)
  259. .get("properties");
  260. optionsProps.forEach(opt => {
  261. // Search for same keys in the existing object
  262. const existingProps = j(currentProps)
  263. .find(j.Identifier)
  264. .filter(path => opt.key.value === path.value.name);
  265. if (existingProps.size()) {
  266. // Replacing values for the same key
  267. existingProps.forEach(path => {
  268. j(path.parent).replaceWith(opt);
  269. });
  270. } else {
  271. // Adding new key:values
  272. currentProps.value.push(opt);
  273. }
  274. });
  275. } else {
  276. // Plugin is called without arguments
  277. args.push(j.objectExpression(optionsProps));
  278. }
  279. }
  280. });
  281. } else {
  282. let argumentsArray = [];
  283. if (optionsProps) {
  284. argumentsArray = [j.objectExpression(optionsProps)];
  285. }
  286. const loaderPluginInstance = j.newExpression(
  287. pathsToMemberExpression(j, pluginName.split(".").reverse()),
  288. argumentsArray
  289. );
  290. rootNodePath.value.elements.push(loaderPluginInstance);
  291. }
  292. }
  293. /**
  294. *
  295. * Finds the variable to which a third party plugin is assigned to
  296. *
  297. * @param {any} j — jscodeshift API
  298. * @param {Node} rootNode - `plugins: []` Root Node. See https://github.com/facebook/jscodeshift/wiki/jscodeshift-Documentation#nodepaths
  299. * @param {String} pluginPackageName - ex. `extract-text-plugin`
  300. * @returns {String} variable name - ex. 'const s = require(s) gives "s"`
  301. */
  302. function findVariableToPlugin(j, rootNode, pluginPackageName) {
  303. const moduleVarNames = rootNode
  304. .find(j.VariableDeclarator)
  305. .filter(j.filters.VariableDeclarator.requiresModule(pluginPackageName))
  306. .nodes();
  307. if (moduleVarNames.length === 0) return null;
  308. return moduleVarNames.pop().id.name;
  309. }
  310. /**
  311. *
  312. * Returns true if type is given type
  313. * @param {Node} path - pathNode
  314. * @param {String} type - node type
  315. * @returns {Boolean}
  316. */
  317. function isType(path, type) {
  318. return path.type === type;
  319. }
  320. function findObjWithOneOfKeys(p, keyNames) {
  321. return p.value.properties.reduce((predicate, prop) => {
  322. const name = prop.key.name;
  323. return keyNames.indexOf(name) > -1 || predicate;
  324. }, false);
  325. }
  326. /**
  327. *
  328. * Returns constructed require symbol
  329. * @param {any} j — jscodeshift API
  330. * @param {String} constName - Name of require
  331. * @param {String} packagePath - path of required package
  332. * @returns {Node} - the created ast
  333. */
  334. function getRequire(j, constName, packagePath) {
  335. return j.variableDeclaration("const", [
  336. j.variableDeclarator(
  337. j.identifier(constName),
  338. j.callExpression(j.identifier("require"), [j.literal(packagePath)])
  339. )
  340. ]);
  341. }
  342. /**
  343. *
  344. * Recursively adds an object/property to a node
  345. * @param {any} j — jscodeshift API
  346. * @param {Node} p - AST node
  347. * @param {String} key - key of a key/val object
  348. * @param {Any} value - Any type of object
  349. * @returns {Node} - the created ast
  350. */
  351. function addProperty(j, p, key, value) {
  352. let valForNode;
  353. if (!p) {
  354. return;
  355. }
  356. if (Array.isArray(value)) {
  357. const arr = j.arrayExpression([]);
  358. value.filter(val => val).forEach(val => {
  359. addProperty(j, arr, null, val);
  360. });
  361. valForNode = arr;
  362. } else if (
  363. typeof value === "object" &&
  364. !(value.__paths || value instanceof RegExp)
  365. ) {
  366. // object -> loop through it
  367. let objectExp = j.objectExpression([]);
  368. Object.keys(value).forEach(prop => {
  369. addProperty(j, objectExp, prop, value[prop]);
  370. });
  371. valForNode = objectExp;
  372. } else {
  373. valForNode = createIdentifierOrLiteral(j, value);
  374. }
  375. let pushVal;
  376. if (key) {
  377. pushVal = j.property("init", j.identifier(key), valForNode);
  378. } else {
  379. pushVal = valForNode;
  380. }
  381. if (p.properties) {
  382. p.properties.push(pushVal);
  383. return p;
  384. }
  385. if (p.value && p.value.properties) {
  386. p.value.properties.push(pushVal);
  387. return p;
  388. }
  389. if (p.elements) {
  390. p.elements.push(pushVal);
  391. return p;
  392. }
  393. return;
  394. }
  395. module.exports = {
  396. safeTraverse,
  397. createProperty,
  398. findPluginsByName,
  399. findRootNodesByName,
  400. addOrUpdateConfigObject,
  401. findAndRemovePluginByName,
  402. createOrUpdatePluginByName,
  403. findVariableToPlugin,
  404. findPluginsArrayAndRemoveIfEmpty,
  405. isType,
  406. createLiteral,
  407. createIdentifierOrLiteral,
  408. findObjWithOneOfKeys,
  409. getRequire,
  410. addProperty
  411. };