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