import.macro.js 7.9 KB

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