JSXElement.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. 'use strict';
  8. const Collection = require('../Collection');
  9. const NodeCollection = require('./Node');
  10. const assert = require('assert');
  11. const once = require('../utils/once');
  12. const recast = require('recast');
  13. const requiresModule = require('./VariableDeclarator').filters.requiresModule;
  14. const types = recast.types.namedTypes;
  15. const JSXElement = types.JSXElement;
  16. const JSXAttribute = types.JSXAttribute;
  17. const Literal = types.Literal;
  18. /**
  19. * Contains filter methods and mutation methods for processing JSXElements.
  20. * @mixin
  21. */
  22. const globalMethods = {
  23. /**
  24. * Finds all JSXElements optionally filtered by name
  25. *
  26. * @param {string} name
  27. * @return {Collection}
  28. */
  29. findJSXElements: function(name) {
  30. const nameFilter = name && {openingElement: {name: {name: name}}};
  31. return this.find(JSXElement, nameFilter);
  32. },
  33. /**
  34. * Finds all JSXElements by module name. Given
  35. *
  36. * var Bar = require('Foo');
  37. * <Bar />
  38. *
  39. * findJSXElementsByModuleName('Foo') will find <Bar />, without having to
  40. * know the variable name.
  41. */
  42. findJSXElementsByModuleName: function(moduleName) {
  43. assert.ok(
  44. moduleName && typeof moduleName === 'string',
  45. 'findJSXElementsByModuleName(...) needs a name to look for'
  46. );
  47. return this.find(types.VariableDeclarator)
  48. .filter(requiresModule(moduleName))
  49. .map(function(path) {
  50. const id = path.value.id.name;
  51. if (id) {
  52. return Collection.fromPaths([path])
  53. .closestScope()
  54. .findJSXElements(id)
  55. .paths();
  56. }
  57. });
  58. }
  59. };
  60. const filterMethods = {
  61. /**
  62. * Filter method for attributes.
  63. *
  64. * @param {Object} attributeFilter
  65. * @return {function}
  66. */
  67. hasAttributes: function(attributeFilter) {
  68. const attributeNames = Object.keys(attributeFilter);
  69. return function filter(path) {
  70. if (!JSXElement.check(path.value)) {
  71. return false;
  72. }
  73. const elementAttributes = Object.create(null);
  74. path.value.openingElement.attributes.forEach(function(attr) {
  75. if (!JSXAttribute.check(attr) ||
  76. !(attr.name.name in attributeFilter)) {
  77. return;
  78. }
  79. elementAttributes[attr.name.name] = attr;
  80. });
  81. return attributeNames.every(function(name) {
  82. if (!(name in elementAttributes) ){
  83. return false;
  84. }
  85. const value = elementAttributes[name].value;
  86. const expected = attributeFilter[name];
  87. // Only when value is truthy access it's properties
  88. const actual = !value
  89. ? value
  90. : Literal.check(value)
  91. ? value.value
  92. : value.expression;
  93. if (typeof expected === 'function') {
  94. return expected(actual);
  95. }
  96. // Literal attribute values are always strings
  97. return String(expected) === actual;
  98. });
  99. };
  100. },
  101. /**
  102. * Filter elements which contain a specific child type
  103. *
  104. * @param {string} name
  105. * @return {function}
  106. */
  107. hasChildren: function(name) {
  108. return function filter(path) {
  109. return JSXElement.check(path.value) &&
  110. path.value.children.some(
  111. child => JSXElement.check(child) &&
  112. child.openingElement.name.name === name
  113. );
  114. };
  115. }
  116. };
  117. /**
  118. * @mixin
  119. */
  120. const traversalMethods = {
  121. /**
  122. * Returns all child nodes, including literals and expressions.
  123. *
  124. * @return {Collection}
  125. */
  126. childNodes: function() {
  127. const paths = [];
  128. this.forEach(function(path) {
  129. const children = path.get('children');
  130. const l = children.value.length;
  131. for (let i = 0; i < l; i++) {
  132. paths.push(children.get(i));
  133. }
  134. });
  135. return Collection.fromPaths(paths, this);
  136. },
  137. /**
  138. * Returns all children that are JSXElements.
  139. *
  140. * @return {JSXElementCollection}
  141. */
  142. childElements: function() {
  143. const paths = [];
  144. this.forEach(function(path) {
  145. const children = path.get('children');
  146. const l = children.value.length;
  147. for (let i = 0; i < l; i++) {
  148. if (types.JSXElement.check(children.value[i])) {
  149. paths.push(children.get(i));
  150. }
  151. }
  152. });
  153. return Collection.fromPaths(paths, this, JSXElement);
  154. },
  155. };
  156. const mappingMethods = {
  157. /**
  158. * Given a JSXElement, returns its "root" name. E.g. it would return "Foo" for
  159. * both <Foo /> and <Foo.Bar />.
  160. *
  161. * @param {NodePath} path
  162. * @return {string}
  163. */
  164. getRootName: function(path) {
  165. let name = path.value.openingElement.name;
  166. while (types.JSXMemberExpression.check(name)) {
  167. name = name.object;
  168. }
  169. return name && name.name || null;
  170. }
  171. };
  172. function register() {
  173. NodeCollection.register();
  174. Collection.registerMethods(globalMethods, types.Node);
  175. Collection.registerMethods(traversalMethods, JSXElement);
  176. }
  177. exports.register = once(register);
  178. exports.filters = filterMethods;
  179. exports.mappings = mappingMethods;