import.macro.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. const { createMacro, MacroError } = require('babel-plugin-macros')
  2. const { addNamed } = require('@babel/helper-module-imports')
  3. module.exports = createMacro(importer, {
  4. configName: 'fontawesome-svg-core'
  5. })
  6. const styles = [
  7. 'solid',
  8. 'regular',
  9. 'light',
  10. 'thin',
  11. 'duotone',
  12. 'brands'
  13. ]
  14. const macroNames = [
  15. ...styles,
  16. 'icon'
  17. ]
  18. const families = [
  19. 'classic',
  20. 'duotone',
  21. 'sharp'
  22. ]
  23. function importer ({references, state, babel, source, config}) {
  24. const license = (config !== undefined ? config.license : 'free')
  25. if (!['free', 'pro'].includes(license)) {
  26. throw new Error(
  27. "config license must be either 'free' or 'pro'"
  28. )
  29. }
  30. Object.keys(references).forEach((key) => {
  31. replace({
  32. macroName: key,
  33. license,
  34. references: references[key],
  35. state,
  36. babel,
  37. source
  38. })
  39. })
  40. }
  41. function replace ({ macroName, license, references, state, babel, source }) {
  42. references.forEach((nodePath) => {
  43. const {iconName, style, family} = resolveReplacement({ nodePath, babel, state, macroName })
  44. const name = `fa${capitalize(camelCase(iconName))}`
  45. const importFrom = getImport({family, style, license, name})
  46. const importName = addNamed(nodePath, name, importFrom)
  47. nodePath.parentPath.replaceWith(importName)
  48. })
  49. }
  50. function getImport ({family, style, license, name}) {
  51. if (family) {
  52. return `@fortawesome/${family.toLowerCase()}-${style}-svg-icons/${name}`
  53. } else if (style === 'brands') {
  54. return `@fortawesome/free-brands-svg-icons/${name}`
  55. } else {
  56. return `@fortawesome/${license}-${style}-svg-icons/${name}`
  57. }
  58. }
  59. function resolveReplacement ({ nodePath, babel, state, macroName }) {
  60. if('icon' === macroName) {
  61. return resolveReplacementIcon({ nodePath, babel, state, macroName })
  62. } else {
  63. return resolveReplacementLegacyStyle({ nodePath, babel, state, macroName })
  64. }
  65. }
  66. // The macros corresonding to legacy style names: solid(), regular(), light(), thin(), duotone(), brands().
  67. function resolveReplacementLegacyStyle({ nodePath, babel, state, macroName }) {
  68. const { types: t } = babel
  69. const { parentPath } = nodePath
  70. if (!styles.includes(macroName)) {
  71. throw parentPath.buildCodeFrameError(
  72. `${macroName} is not a valid macro name. Use one of ${macroNames.join(', ')}`,
  73. MacroError
  74. )
  75. }
  76. if (parentPath.node.arguments) {
  77. if (parentPath.node.arguments.length < 1) {
  78. throw parentPath.buildCodeFrameError(
  79. `Received an invalid number of arguments for ${macroName} macro: must be exactly 1`,
  80. MacroError
  81. )
  82. }
  83. if (parentPath.node.arguments.length > 1) {
  84. throw parentPath.buildCodeFrameError(
  85. `Received an invalid number of arguments for ${macroName} macro: must be exactly 1`,
  86. MacroError
  87. )
  88. }
  89. if (
  90. (parentPath.node.arguments.length === 1 ||
  91. parentPath.node.arguments.length === 2) &&
  92. t.isStringLiteral(parentPath.node.arguments[0]) &&
  93. nodePath.parentPath.node.arguments[0].value.startsWith('fa-')
  94. ) {
  95. throw parentPath.buildCodeFrameError(
  96. `Don't begin the icon name with fa-, just use ${nodePath.parentPath.node.arguments[0].value.slice(3)}`,
  97. MacroError
  98. )
  99. }
  100. if ((parentPath.node.arguments.length === 1 ||
  101. parentPath.node.arguments.length === 2) &&
  102. !t.isStringLiteral(parentPath.node.arguments[0])) {
  103. throw parentPath.buildCodeFrameError(
  104. 'Only string literals are supported when referencing icons (use a string here instead)',
  105. MacroError
  106. )
  107. }
  108. } else {
  109. throw parentPath.buildCodeFrameError(
  110. 'Pass the icon name you would like to import as an argument.',
  111. MacroError
  112. )
  113. }
  114. return {
  115. iconName: nodePath.parentPath.node.arguments[0].value,
  116. style: macroName,
  117. family: undefined
  118. }
  119. }
  120. // The icon() macro.
  121. function resolveReplacementIcon ({ nodePath, babel, state, macroName }) {
  122. const { types: t } = babel
  123. const { parentPath } = nodePath
  124. if ('icon' !== macroName) {
  125. throw parentPath.buildCodeFrameError(
  126. `${macroName} is not a valid macro name. Use one of ${macroNames.join(', ')}`,
  127. MacroError
  128. )
  129. }
  130. if (parentPath.node.arguments.length !== 1) {
  131. throw parentPath.buildCodeFrameError(
  132. `Received an invalid number of arguments for ${macroName} macro: must be exactly 1`,
  133. MacroError
  134. )
  135. }
  136. if (!t.isObjectExpression(parentPath.node.arguments[0])) {
  137. throw parentPath.buildCodeFrameError(
  138. 'Only object expressions are supported when referencing icons with this macro, like this: { name: \'star\' }',
  139. MacroError
  140. )
  141. }
  142. const properties = (parentPath.node.arguments[0].properties || [])
  143. const namePropIndex = properties.findIndex((prop) => 'name' === prop.key.name)
  144. const name = namePropIndex >= 0
  145. ? getStringLiteralPropertyValue(t, parentPath, parentPath.node.arguments[0].properties[namePropIndex])
  146. : undefined
  147. if(!name) {
  148. throw parentPath.buildCodeFrameError(
  149. 'The object argument to the icon() macro must have a name property',
  150. MacroError
  151. )
  152. }
  153. const stylePropIndex = properties.findIndex((prop) => 'style' === prop.key.name)
  154. let style = stylePropIndex >= 0
  155. ? getStringLiteralPropertyValue(t, parentPath, parentPath.node.arguments[0].properties[stylePropIndex])
  156. : undefined
  157. if(style && !styles.includes(style)) {
  158. throw parentPath.buildCodeFrameError(
  159. `Invalid style name: ${style}. It must be one of the following: ${styles.join(', ')}`,
  160. MacroError
  161. )
  162. }
  163. const familyPropIndex = properties.findIndex((prop) => 'family' === prop.key.name)
  164. let family = familyPropIndex >= 0
  165. ? getStringLiteralPropertyValue(t, parentPath, parentPath.node.arguments[0].properties[familyPropIndex])
  166. : undefined
  167. if(family && !families.includes(family)) {
  168. throw parentPath.buildCodeFrameError(
  169. `Invalid family name: ${family}. It must be one of the following: ${families.join(', ')}`,
  170. MacroError
  171. )
  172. }
  173. if('duotone' === style && family && 'classic' !== family) {
  174. throw parentPath.buildCodeFrameError(
  175. `duotone cannot be used as a style name with any family other than classic`,
  176. MacroError
  177. )
  178. }
  179. if('brands' === style && family && 'classic' !== family) {
  180. throw parentPath.buildCodeFrameError(
  181. `brands cannot be used as a style name with any family other than classic`,
  182. MacroError
  183. )
  184. }
  185. if(family && !style) {
  186. throw parentPath.buildCodeFrameError(
  187. `When a family is specified, a style must also be specified`,
  188. MacroError
  189. )
  190. }
  191. if('duotone' === style || 'duotone' === family) {
  192. family = undefined
  193. style = 'duotone'
  194. }
  195. if('brands' === style) {
  196. family = undefined
  197. }
  198. // defaults
  199. if(!style) {
  200. style = 'solid'
  201. }
  202. if('classic' === family) {
  203. family = undefined
  204. }
  205. return {
  206. iconName: name,
  207. family,
  208. style
  209. }
  210. }
  211. function getStringLiteralPropertyValue(t, parentPath, property) {
  212. if(!('object' === typeof t && 'function' === typeof t.isStringLiteral)) {
  213. throw Error("ERROR: invalid babel-types arg. This is probably a programming error in import.macro")
  214. }
  215. if(!('object' === typeof property && 'object' === typeof property.value && 'object' == typeof property.key)) {
  216. throw Error("ERROR: invalid babel property arg. This is probably a programming error in import.macro")
  217. }
  218. if(!('object' === typeof parentPath && 'function' === typeof parentPath.buildCodeFrameError)) {
  219. throw Error("ERROR: invalid babel parentPath arg. This is probably a programming error in import.macro")
  220. }
  221. if(!t.isStringLiteral(property.value)) {
  222. throw parentPath.buildCodeFrameError(
  223. `Only string literals are supported for the ${property.key.name} property (use a string here instead)`,
  224. MacroError
  225. )
  226. }
  227. return property.value.value
  228. }
  229. function capitalize (str) {
  230. return str[0].toUpperCase() + str.slice(1)
  231. }
  232. function camelCase (str) {
  233. return str
  234. .split('-')
  235. .map((s, index) => {
  236. return (
  237. (index === 0 ? s[0].toLowerCase() : s[0].toUpperCase()) +
  238. s.slice(1).toLowerCase()
  239. )
  240. })
  241. .join('')
  242. }