loaders.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. const utils = require("../../utils/ast-utils");
  2. /**
  3. *
  4. * Transform for loaders. Transforms pre- and postLoaders into enforce options,
  5. * moves loader configuration into rules array, transforms query strings and
  6. * props into loader options, and adds -loader suffix to loader names.
  7. *
  8. * @param {Object} j - jscodeshift top-level import
  9. * @param {Node} ast - jscodeshift ast to transform
  10. * @returns {Node} ast - jscodeshift ast
  11. */
  12. module.exports = function(j, ast) {
  13. /**
  14. * Creates an Array expression out of loaders string
  15. *
  16. *
  17. * For syntaxes like
  18. *
  19. * {
  20. * loader: 'style!css`
  21. * }
  22. *
  23. * or
  24. *
  25. * {
  26. * loaders: ['style', 'css']
  27. * }
  28. *
  29. * or
  30. *
  31. * loaders: [{
  32. * loader: 'style'
  33. * },
  34. * {
  35. * loader: 'css',
  36. * }]
  37. *
  38. * it should generate
  39. *
  40. * {
  41. * use: [{
  42. * loader: 'style'
  43. * },
  44. * {
  45. * loader: 'css'
  46. * }]
  47. * }
  48. *
  49. * @param {Node} path - object expression ast
  50. * @returns {Node} path - object expression ast with array expression instead of loaders string
  51. */
  52. const createArrayExpressionFromArray = function(path) {
  53. const value = path.value;
  54. // Find paths with `loaders` keys in the given Object
  55. const paths = value.properties.filter(prop =>
  56. prop.key.name.startsWith("loader")
  57. );
  58. // For each pair of key and value
  59. paths.forEach(pair => {
  60. // Replace 'loaders' Identifier with 'use'
  61. pair.key.name = "use";
  62. // If the value is an Array
  63. if (pair.value.type === j.ArrayExpression.name) {
  64. // replace its elements
  65. const pairValue = pair.value;
  66. pair.value = j.arrayExpression(
  67. pairValue.elements.map(arrElement => {
  68. // If items of the array are Strings
  69. if (arrElement.type === j.Literal.name) {
  70. // Replace with `{ loader: LOADER }` Object
  71. return j.objectExpression([
  72. utils.createProperty(j, "loader", arrElement.value)
  73. ]);
  74. }
  75. // otherwise keep the existing element
  76. return arrElement;
  77. })
  78. );
  79. // If the value is String of loaders like 'style!css'
  80. } else if (pair.value.type === j.Literal.name) {
  81. // Replace it with Array expression of loaders
  82. const literalValue = pair.value;
  83. pair.value = j.arrayExpression(
  84. literalValue.value.split("!").map(loader => {
  85. return j.objectExpression([
  86. utils.createProperty(j, "loader", loader)
  87. ]);
  88. })
  89. );
  90. }
  91. });
  92. return path;
  93. };
  94. /**
  95. *
  96. * Puts query parameters from loader value into options object
  97. *
  98. * @param {Node} p - object expression ast for loader object
  99. * @returns {Node} objectExpression - an new object expression ast containing the query parameters
  100. */
  101. const createLoaderWithQuery = p => {
  102. let properties = p.value.properties;
  103. let loaderValue = properties.reduce(
  104. (val, prop) => (prop.key.name === "loader" ? prop.value.value : val),
  105. ""
  106. );
  107. let loader = loaderValue.split("?")[0];
  108. let query = loaderValue.split("?")[1];
  109. let options = query.split("&").map(option => {
  110. const param = option.split("=");
  111. const key = param[0];
  112. const val = param[1] || true; // No value in query string means it is truthy value
  113. return j.objectProperty(j.identifier(key), utils.createLiteral(j, val));
  114. });
  115. let loaderProp = utils.createProperty(j, "loader", loader);
  116. let queryProp = j.property(
  117. "init",
  118. j.identifier("options"),
  119. j.objectExpression(options)
  120. );
  121. return j.objectExpression([loaderProp, queryProp]);
  122. };
  123. /**
  124. *
  125. * Determine whether a loader has a query string
  126. *
  127. * @param {Node} p - object expression ast for loader object
  128. * @returns {Boolean} hasLoaderQueryString - whether the loader object contains a query string
  129. */
  130. const findLoaderWithQueryString = p => {
  131. return p.value.properties.reduce((predicate, prop) => {
  132. return (
  133. (utils.safeTraverse(prop, ["value", "value", "indexOf"]) &&
  134. prop.value.value.indexOf("?") > -1) ||
  135. predicate
  136. );
  137. }, false);
  138. };
  139. /**
  140. * Check if the identifier is the `loaders` prop in the `module` object.
  141. * If the path value is `loaders` and it’s located in `module` object
  142. * we assume it’s the loader's section.
  143. *
  144. * @param {Node} path - identifier ast
  145. * @returns {Boolean} isLoadersProp - whether the identifier is the `loaders` prop in the `module` object
  146. */
  147. const checkForLoader = path =>
  148. path.value.name === "loaders" &&
  149. utils.safeTraverse(path, [
  150. "parent",
  151. "parent",
  152. "parent",
  153. "node",
  154. "key",
  155. "name"
  156. ]) === "module";
  157. /**
  158. * Puts pre- or postLoader into `loaders` object and adds the appropriate `enforce` property
  159. *
  160. * @param {Node} p - object expression ast that has a key for either 'preLoaders' or 'postLoaders'
  161. * @returns {Node} p - object expression with a `loaders` object and appropriate `enforce` properties
  162. */
  163. const fitIntoLoaders = p => {
  164. let loaders;
  165. p.value.properties.map(prop => {
  166. const keyName = prop.key.name;
  167. if (keyName === "loaders") {
  168. loaders = prop.value;
  169. }
  170. });
  171. p.value.properties.map(prop => {
  172. const keyName = prop.key.name;
  173. if (keyName !== "loaders") {
  174. const enforceVal = keyName === "preLoaders" ? "pre" : "post";
  175. prop.value.elements.map(elem => {
  176. elem.properties.push(utils.createProperty(j, "enforce", enforceVal));
  177. if (loaders && loaders.type === "ArrayExpression") {
  178. loaders.elements.push(elem);
  179. } else {
  180. prop.key.name = "loaders";
  181. }
  182. });
  183. }
  184. });
  185. if (loaders) {
  186. p.value.properties = p.value.properties.filter(
  187. prop => prop.key.name === "loaders"
  188. );
  189. }
  190. return p;
  191. };
  192. /**
  193. * Find pre and postLoaders in the ast and put them into the `loaders` array
  194. *
  195. * @returns {Node} ast - jscodeshift ast
  196. */
  197. const prepostLoaders = () =>
  198. ast
  199. .find(j.ObjectExpression)
  200. .filter(p => utils.findObjWithOneOfKeys(p, ["preLoaders", "postLoaders"]))
  201. .forEach(fitIntoLoaders);
  202. /**
  203. * Convert top level `loaders` to `rules`
  204. *
  205. * @returns {Node} ast - jscodeshift ast
  206. */
  207. const loadersToRules = () =>
  208. ast
  209. .find(j.Identifier)
  210. .filter(checkForLoader)
  211. .forEach(p => (p.value.name = "rules"));
  212. /**
  213. * Convert `loader` and `loaders` to Array of {Rule.Use}
  214. *
  215. * @returns {Node} ast - jscodeshift ast
  216. */
  217. const loadersToArrayExpression = () =>
  218. ast
  219. .find(j.ObjectExpression)
  220. .filter(path => utils.findObjWithOneOfKeys(path, ["loader", "loaders"]))
  221. .filter(
  222. path =>
  223. utils.safeTraverse(path, [
  224. "parent",
  225. "parent",
  226. "node",
  227. "key",
  228. "name"
  229. ]) === "rules"
  230. )
  231. .forEach(createArrayExpressionFromArray);
  232. /**
  233. * Find loaders with options encoded as a query string and replace the string with an options object
  234. *
  235. * i.e. for loader like
  236. *
  237. * {
  238. * loader: 'css?modules&importLoaders=1&string=test123'
  239. * }
  240. *
  241. * it should generate
  242. * {
  243. * loader: 'css-loader',
  244. * options: {
  245. * modules: true,
  246. * importLoaders: 1,
  247. * string: 'test123'
  248. * }
  249. * }
  250. *
  251. * @returns {Node} ast - jscodeshift ast
  252. */
  253. const loaderWithQueryParam = () =>
  254. ast
  255. .find(j.ObjectExpression)
  256. .filter(p => utils.findObjWithOneOfKeys(p, ["loader"]))
  257. .filter(findLoaderWithQueryString)
  258. .replaceWith(createLoaderWithQuery);
  259. /**
  260. * Find nodes with a `query` key and replace it with `options`
  261. *
  262. * i.e. for
  263. * {
  264. * query: { ... }
  265. * }
  266. *
  267. * it should generate
  268. *
  269. * {
  270. * options: { ... }
  271. * }
  272. *
  273. * @returns {Node} ast - jscodeshift ast
  274. */
  275. const loaderWithQueryProp = () =>
  276. ast
  277. .find(j.Identifier)
  278. .filter(p => p.value.name === "query")
  279. .replaceWith(j.identifier("options"));
  280. /**
  281. * Add required `-loader` suffix to a loader with missing suffix
  282. * e.g. for `babel` it should generate `babel-loader`
  283. *
  284. * @returns {Node} ast - jscodeshift ast
  285. */
  286. const addLoaderSuffix = () =>
  287. ast.find(j.ObjectExpression).forEach(path => {
  288. path.value.properties.forEach(prop => {
  289. if (
  290. prop.key.name === "loader" &&
  291. utils.safeTraverse(prop, ["value", "value"]) &&
  292. !prop.value.value.endsWith("-loader")
  293. ) {
  294. prop.value = j.literal(prop.value.value + "-loader");
  295. }
  296. });
  297. });
  298. /**
  299. *
  300. * Puts options object outside use object into use object
  301. *
  302. * @param {Node} p - object expression ast that has a key for either 'options' or 'use'
  303. * @returns {Node} objectExpression - an use object expression ast containing the options and loader
  304. */
  305. const fitOptionsToUse = p => {
  306. let options;
  307. p.value.properties.forEach(prop => {
  308. const keyName = prop.key.name;
  309. if (keyName === "options") {
  310. options = prop;
  311. }
  312. });
  313. if (options) {
  314. p.value.properties = p.value.properties.filter(
  315. prop => prop.key.name !== "options"
  316. );
  317. p.value.properties.forEach(prop => {
  318. const keyName = prop.key.name;
  319. if (keyName === "use") {
  320. prop.value.elements[0].properties.push(options);
  321. }
  322. });
  323. }
  324. return p;
  325. };
  326. /**
  327. * Move `options` inside the Array of {Rule.Use}
  328. *
  329. * @returns {Node} ast - jscodeshift ast
  330. */
  331. const moveOptionsToUse = () =>
  332. ast
  333. .find(j.ObjectExpression)
  334. .filter(p => utils.findObjWithOneOfKeys(p, ["use"]))
  335. .forEach(fitOptionsToUse);
  336. const transforms = [
  337. prepostLoaders,
  338. loadersToRules,
  339. loadersToArrayExpression,
  340. loaderWithQueryParam,
  341. loaderWithQueryProp,
  342. addLoaderSuffix,
  343. moveOptionsToUse
  344. ];
  345. transforms.forEach(t => t());
  346. return ast;
  347. };