syntax-tree / estree-util-build-jsx
Showing 2 of 4 files from the diff.
Newly tracked file
lib/index.js created.
Other files ignored by Codecov
package.json has changed.
tsconfig.json has changed.

@@ -0,0 +1,544 @@
Loading
1 +
/**
2 +
 * @typedef {import('estree-jsx').Node} Node
3 +
 * @typedef {import('estree-jsx').Comment} Comment
4 +
 * @typedef {import('estree-jsx').Expression} Expression
5 +
 * @typedef {import('estree-jsx').Pattern} Pattern
6 +
 * @typedef {import('estree-jsx').ObjectExpression} ObjectExpression
7 +
 * @typedef {import('estree-jsx').Property} Property
8 +
 * @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
9 +
 * @typedef {import('estree-jsx').SpreadElement} SpreadElement
10 +
 * @typedef {import('estree-jsx').MemberExpression} MemberExpression
11 +
 * @typedef {import('estree-jsx').Literal} Literal
12 +
 * @typedef {import('estree-jsx').Identifier} Identifier
13 +
 * @typedef {import('estree-jsx').JSXElement} JSXElement
14 +
 * @typedef {import('estree-jsx').JSXFragment} JSXFragment
15 +
 * @typedef {import('estree-jsx').JSXText} JSXText
16 +
 * @typedef {import('estree-jsx').JSXExpressionContainer} JSXExpressionContainer
17 +
 * @typedef {import('estree-jsx').JSXEmptyExpression} JSXEmptyExpression
18 +
 * @typedef {import('estree-jsx').JSXSpreadChild} JSXSpreadChild
19 +
 * @typedef {import('estree-jsx').JSXAttribute} JSXAttribute
20 +
 * @typedef {import('estree-jsx').JSXSpreadAttribute} JSXSpreadAttribute
21 +
 * @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
22 +
 * @typedef {import('estree-jsx').JSXNamespacedName} JSXNamespacedName
23 +
 * @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
24 +
 *
25 +
 * @typedef {import('estree-walker').SyncHandler} SyncHandler
26 +
 *
27 +
 * @typedef Options
28 +
 * @property {'automatic'|'classic'} [runtime='classic']
29 +
 * @property {string} [importSource='react']
30 +
 * @property {string} [pragma='React.createElement']
31 +
 * @property {string} [pragmaFrag='React.Fragment']
32 +
 * @property {boolean} [development=false]
33 +
 * @property {string} [filePath]
34 +
 *
35 +
 * @typedef Annotations
36 +
 * @property {'automatic'|'classic'} [jsxRuntime]
37 +
 * @property {string} [jsx]
38 +
 * @property {string} [jsxFrag]
39 +
 * @property {string} [jsxImportSource]
40 +
 */
41 +
42 +
import {walk} from 'estree-walker'
43 +
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
44 +
45 +
const regex = /@(jsx|jsxFrag|jsxImportSource|jsxRuntime)\s+(\S+)/g
46 +
47 +
/**
48 +
 * @template {Node} T
49 +
 * @param {T} tree
50 +
 * @param {Options} [options={}]
51 +
 * @returns {T}
52 +
 */
53 +
export function buildJsx(tree, options = {}) {
54 +
  let automatic = options.runtime === 'automatic'
55 +
  /** @type {Annotations} */
56 +
  const annotations = {}
57 +
  /** @type {{fragment?: boolean, jsx?: boolean, jsxs?: boolean, jsxDEV?: boolean}} */
58 +
  const imports = {}
59 +
60 +
  walk(tree, {
61 +
    // @ts-expect-error: types are wrong.
62 +
    enter(/** @type {Node} */ node) {
63 +
      if (node.type === 'Program') {
64 +
        const comments = node.comments || []
65 +
        let index = -1
66 +
67 +
        while (++index < comments.length) {
68 +
          regex.lastIndex = 0
69 +
70 +
          let match = regex.exec(comments[index].value)
71 +
72 +
          while (match) {
73 +
            // @ts-expect-error: indexable.
74 +
            annotations[match[1]] = match[2]
75 +
            match = regex.exec(comments[index].value)
76 +
          }
77 +
        }
78 +
79 +
        if (annotations.jsxRuntime) {
80 +
          if (annotations.jsxRuntime === 'automatic') {
81 +
            automatic = true
82 +
83 +
            if (annotations.jsx) {
84 +
              throw new Error('Unexpected `@jsx` pragma w/ automatic runtime')
85 +
            }
86 +
87 +
            if (annotations.jsxFrag) {
88 +
              throw new Error(
89 +
                'Unexpected `@jsxFrag` pragma w/ automatic runtime'
90 +
              )
91 +
            }
92 +
          } else if (annotations.jsxRuntime === 'classic') {
93 +
            automatic = false
94 +
95 +
            if (annotations.jsxImportSource) {
96 +
              throw new Error(
97 +
                'Unexpected `@jsxImportSource` w/ classic runtime'
98 +
              )
99 +
            }
100 +
          } else {
101 +
            throw new Error(
102 +
              'Unexpected `jsxRuntime` `' +
103 +
                annotations.jsxRuntime +
104 +
                '`, expected `automatic` or `classic`'
105 +
            )
106 +
          }
107 +
        }
108 +
      }
109 +
    },
110 +
    // @ts-expect-error: types are wrong.
111 +
    // eslint-disable-next-line complexity
112 +
    leave(/** @type {Node} */ node) {
113 +
      if (node.type === 'Program') {
114 +
        /** @type {Array<ImportSpecifier>} */
115 +
        const specifiers = []
116 +
117 +
        if (imports.fragment) {
118 +
          specifiers.push({
119 +
            type: 'ImportSpecifier',
120 +
            imported: {type: 'Identifier', name: 'Fragment'},
121 +
            local: {type: 'Identifier', name: '_Fragment'}
122 +
          })
123 +
        }
124 +
125 +
        if (imports.jsx) {
126 +
          specifiers.push({
127 +
            type: 'ImportSpecifier',
128 +
            imported: {type: 'Identifier', name: 'jsx'},
129 +
            local: {type: 'Identifier', name: '_jsx'}
130 +
          })
131 +
        }
132 +
133 +
        if (imports.jsxs) {
134 +
          specifiers.push({
135 +
            type: 'ImportSpecifier',
136 +
            imported: {type: 'Identifier', name: 'jsxs'},
137 +
            local: {type: 'Identifier', name: '_jsxs'}
138 +
          })
139 +
        }
140 +
141 +
        if (imports.jsxDEV) {
142 +
          specifiers.push({
143 +
            type: 'ImportSpecifier',
144 +
            imported: {type: 'Identifier', name: 'jsxDEV'},
145 +
            local: {type: 'Identifier', name: '_jsxDEV'}
146 +
          })
147 +
        }
148 +
149 +
        if (specifiers.length > 0) {
150 +
          node.body.unshift({
151 +
            type: 'ImportDeclaration',
152 +
            specifiers,
153 +
            source: {
154 +
              type: 'Literal',
155 +
              value:
156 +
                (annotations.jsxImportSource ||
157 +
                  options.importSource ||
158 +
                  'react') +
159 +
                (options.development ? '/jsx-dev-runtime' : '/jsx-runtime')
160 +
            }
161 +
          })
162 +
        }
163 +
      }
164 +
165 +
      if (node.type !== 'JSXElement' && node.type !== 'JSXFragment') {
166 +
        return
167 +
      }
168 +
169 +
      /** @type {Array<Expression>} */
170 +
      const children = []
171 +
      let index = -1
172 +
173 +
      // Figure out `children`.
174 +
      while (++index < node.children.length) {
175 +
        const child = node.children[index]
176 +
177 +
        if (child.type === 'JSXExpressionContainer') {
178 +
          // Ignore empty expressions.
179 +
          if (child.expression.type !== 'JSXEmptyExpression') {
180 +
            children.push(child.expression)
181 +
          }
182 +
        } else if (child.type === 'JSXText') {
183 +
          const value = child.value
184 +
            // Replace tabs w/ spaces.
185 +
            .replace(/\t/g, ' ')
186 +
            // Use line feeds, drop spaces around them.
187 +
            .replace(/ *(\r?\n|\r) */g, '\n')
188 +
            // Collapse multiple line feeds.
189 +
            .replace(/\n+/g, '\n')
190 +
            // Drop final line feeds.
191 +
            .replace(/\n+$/, '')
192 +
            // Replace line feeds with spaces.
193 +
            .replace(/\n/g, ' ')
194 +
195 +
          // Ignore collapsible text.
196 +
          if (value) {
197 +
            children.push(create(child, {type: 'Literal', value}))
198 +
          }
199 +
        } else {
200 +
          // @ts-expect-error JSX{Element,Fragment} have already been compiled,
201 +
          // and `JSXSpreadChild` is not supported in Babel either, so ignore
202 +
          // it.
203 +
          children.push(child)
204 +
        }
205 +
      }
206 +
207 +
      /** @type {MemberExpression|Literal|Identifier} */
208 +
      let name
209 +
      /** @type {Array<Property>} */
210 +
      let fields = []
211 +
      /** @type {Array<Expression>} */
212 +
      const objects = []
213 +
      /** @type {Array<Expression|SpreadElement>} */
214 +
      let parameters = []
215 +
      /** @type {Expression|undefined} */
216 +
      let key
217 +
218 +
      // Do the stuff needed for elements.
219 +
      if (node.type === 'JSXElement') {
220 +
        name = toIdentifier(node.openingElement.name)
221 +
222 +
        // If the name could be an identifier, but start with a lowercase letter,
223 +
        // it’s not a component.
224 +
        if (name.type === 'Identifier' && /^[a-z]/.test(name.name)) {
225 +
          name = create(name, {type: 'Literal', value: name.name})
226 +
        }
227 +
228 +
        /** @type {boolean|undefined} */
229 +
        let spread
230 +
        const attributes = node.openingElement.attributes
231 +
        let index = -1
232 +
233 +
        // Place props in the right order, because we might have duplicates
234 +
        // in them and what’s spread in.
235 +
        while (++index < attributes.length) {
236 +
          const attribute = attributes[index]
237 +
238 +
          if (attribute.type === 'JSXSpreadAttribute') {
239 +
            if (fields.length > 0) {
240 +
              objects.push({type: 'ObjectExpression', properties: fields})
241 +
              fields = []
242 +
            }
243 +
244 +
            objects.push(attribute.argument)
245 +
            spread = true
246 +
          } else {
247 +
            const prop = toProperty(attribute)
248 +
249 +
            if (
250 +
              automatic &&
251 +
              prop.key.type === 'Identifier' &&
252 +
              prop.key.name === 'key'
253 +
            ) {
254 +
              if (spread) {
255 +
                throw new Error(
256 +
                  'Expected `key` to come before any spread expressions'
257 +
                )
258 +
              }
259 +
260 +
              // @ts-expect-error I can’t see object patterns being used as
261 +
              // attribute values? 🤷‍♂️
262 +
              key = prop.value
263 +
            } else {
264 +
              fields.push(prop)
265 +
            }
266 +
          }
267 +
        }
268 +
      }
269 +
      // …and fragments.
270 +
      else if (automatic) {
271 +
        imports.fragment = true
272 +
        name = {type: 'Identifier', name: '_Fragment'}
273 +
      } else {
274 +
        name = toMemberExpression(
275 +
          annotations.jsxFrag || options.pragmaFrag || 'React.Fragment'
276 +
        )
277 +
      }
278 +
279 +
      if (automatic) {
280 +
        if (children.length > 0) {
281 +
          fields.push({
282 +
            type: 'Property',
283 +
            key: {type: 'Identifier', name: 'children'},
284 +
            value:
285 +
              children.length > 1
286 +
                ? {type: 'ArrayExpression', elements: children}
287 +
                : children[0],
288 +
            kind: 'init',
289 +
            method: false,
290 +
            shorthand: false,
291 +
            computed: false
292 +
          })
293 +
        }
294 +
      } else {
295 +
        parameters = children
296 +
      }
297 +
298 +
      if (fields.length > 0) {
299 +
        objects.push({type: 'ObjectExpression', properties: fields})
300 +
      }
301 +
302 +
      /** @type {Expression|undefined} */
303 +
      let props
304 +
      /** @type {MemberExpression|Literal|Identifier} */
305 +
      let callee
306 +
307 +
      if (objects.length > 1) {
308 +
        // Don’t mutate the first object, shallow clone instead.
309 +
        if (objects[0].type !== 'ObjectExpression') {
310 +
          objects.unshift({type: 'ObjectExpression', properties: []})
311 +
        }
312 +
313 +
        props = {
314 +
          type: 'CallExpression',
315 +
          callee: toMemberExpression('Object.assign'),
316 +
          arguments: objects,
317 +
          optional: false
318 +
        }
319 +
      } else if (objects.length > 0) {
320 +
        props = objects[0]
321 +
      }
322 +
323 +
      if (automatic) {
324 +
        parameters.push(props || {type: 'ObjectExpression', properties: []})
325 +
326 +
        if (key) {
327 +
          parameters.push(key)
328 +
        } else if (options.development) {
329 +
          parameters.push({type: 'Identifier', name: 'undefined'})
330 +
        }
331 +
332 +
        const isStaticChildren = children.length > 1
333 +
334 +
        if (options.development) {
335 +
          imports.jsxDEV = true
336 +
          callee = {
337 +
            type: 'Identifier',
338 +
            name: '_jsxDEV'
339 +
          }
340 +
          parameters.push({type: 'Literal', value: isStaticChildren})
341 +
342 +
          /** @type {ObjectExpression} */
343 +
          const source = {
344 +
            type: 'ObjectExpression',
345 +
            properties: [
346 +
              {
347 +
                type: 'Property',
348 +
                method: false,
349 +
                shorthand: false,
350 +
                computed: false,
351 +
                kind: 'init',
352 +
                key: {type: 'Identifier', name: 'fileName'},
353 +
                value: {
354 +
                  type: 'Literal',
355 +
                  value: options.filePath || '<source.js>'
356 +
                }
357 +
              }
358 +
            ]
359 +
          }
360 +
361 +
          if (node.loc) {
362 +
            source.properties.push(
363 +
              {
364 +
                type: 'Property',
365 +
                method: false,
366 +
                shorthand: false,
367 +
                computed: false,
368 +
                kind: 'init',
369 +
                key: {type: 'Identifier', name: 'lineNumber'},
370 +
                value: {type: 'Literal', value: node.loc.start.line}
371 +
              },
372 +
              {
373 +
                type: 'Property',
374 +
                method: false,
375 +
                shorthand: false,
376 +
                computed: false,
377 +
                kind: 'init',
378 +
                key: {type: 'Identifier', name: 'columnNumber'},
379 +
                value: {type: 'Literal', value: node.loc.start.column + 1}
380 +
              }
381 +
            )
382 +
          }
383 +
384 +
          parameters.push(source, {type: 'ThisExpression'})
385 +
        } else if (isStaticChildren) {
386 +
          imports.jsxs = true
387 +
          callee = {type: 'Identifier', name: '_jsxs'}
388 +
        } else {
389 +
          imports.jsx = true
390 +
          callee = {type: 'Identifier', name: '_jsx'}
391 +
        }
392 +
      }
393 +
      // Classic.
394 +
      else {
395 +
        // There are props or children.
396 +
        if (props || parameters.length > 0) {
397 +
          parameters.unshift(props || {type: 'Literal', value: null})
398 +
        }
399 +
400 +
        callee = toMemberExpression(
401 +
          annotations.jsx || options.pragma || 'React.createElement'
402 +
        )
403 +
      }
404 +
405 +
      parameters.unshift(name)
406 +
407 +
      this.replace(
408 +
        create(node, {
409 +
          type: 'CallExpression',
410 +
          callee,
411 +
          arguments: parameters,
412 +
          optional: false
413 +
        })
414 +
      )
415 +
    }
416 +
  })
417 +
418 +
  return tree
419 +
}
420 +
421 +
/**
422 +
 * @param {JSXAttribute} node
423 +
 * @returns {Property}
424 +
 */
425 +
function toProperty(node) {
426 +
  /** @type {Expression} */
427 +
  let value
428 +
429 +
  if (node.value) {
430 +
    if (node.value.type === 'JSXExpressionContainer') {
431 +
      // @ts-expect-error `JSXEmptyExpression` is not allowed in props.
432 +
      value = node.value.expression
433 +
    }
434 +
    // Literal or call expression.
435 +
    else {
436 +
      // @ts-expect-error: JSX{Element,Fragment} are already compiled to
437 +
      // `CallExpression`.
438 +
      value = node.value
439 +
      // @ts-expect-error Remove `raw` so we don’t get character references in
440 +
      // strings.
441 +
      delete value.raw
442 +
    }
443 +
  }
444 +
  // Boolean prop.
445 +
  else {
446 +
    value = {type: 'Literal', value: true}
447 +
  }
448 +
449 +
  return create(node, {
450 +
    type: 'Property',
451 +
    key: toIdentifier(node.name),
452 +
    value,
453 +
    kind: 'init',
454 +
    method: false,
455 +
    shorthand: false,
456 +
    computed: false
457 +
  })
458 +
}
459 +
460 +
/**
461 +
 * @param {JSXMemberExpression|JSXNamespacedName|JSXIdentifier} node
462 +
 * @returns {MemberExpression|Identifier|Literal}
463 +
 */
464 +
function toIdentifier(node) {
465 +
  /** @type {MemberExpression|Identifier|Literal} */
466 +
  let replace
467 +
468 +
  if (node.type === 'JSXMemberExpression') {
469 +
    // `property` is always a `JSXIdentifier`, but it could be something that
470 +
    // isn’t an ES identifier name.
471 +
    const id = toIdentifier(node.property)
472 +
    replace = {
473 +
      type: 'MemberExpression',
474 +
      object: toIdentifier(node.object),
475 +
      property: id,
476 +
      computed: id.type === 'Literal',
477 +
      optional: false
478 +
    }
479 +
  } else if (node.type === 'JSXNamespacedName') {
480 +
    replace = {
481 +
      type: 'Literal',
482 +
      value: node.namespace.name + ':' + node.name.name
483 +
    }
484 +
  }
485 +
  // Must be `JSXIdentifier`.
486 +
  else {
487 +
    replace = isIdentifierName(node.name)
488 +
      ? {type: 'Identifier', name: node.name}
489 +
      : {type: 'Literal', value: node.name}
490 +
  }
491 +
492 +
  return create(node, replace)
493 +
}
494 +
495 +
/**
496 +
 * @param {string} id
497 +
 * @returns {Identifier|Literal|MemberExpression}
498 +
 */
499 +
function toMemberExpression(id) {
500 +
  const identifiers = id.split('.')
501 +
  let index = -1
502 +
  /** @type {Identifier|Literal|MemberExpression|undefined} */
503 +
  let result
504 +
505 +
  while (++index < identifiers.length) {
506 +
    /** @type {Identifier|Literal} */
507 +
    const prop = isIdentifierName(identifiers[index])
508 +
      ? {type: 'Identifier', name: identifiers[index]}
509 +
      : {type: 'Literal', value: identifiers[index]}
510 +
    result = result
511 +
      ? {
512 +
          type: 'MemberExpression',
513 +
          object: result,
514 +
          property: prop,
515 +
          computed: Boolean(index && prop.type === 'Literal'),
516 +
          optional: false
517 +
        }
518 +
      : prop
519 +
  }
520 +
521 +
  // @ts-expect-error: always a result.
522 +
  return result
523 +
}
524 +
525 +
/**
526 +
 * @template {Node} T
527 +
 * @param {Node} from
528 +
 * @param {T} node
529 +
 * @returns {T}
530 +
 */
531 +
function create(from, node) {
532 +
  const fields = ['start', 'end', 'loc', 'range', 'comments']
533 +
  let index = -1
534 +
535 +
  while (++index < fields.length) {
536 +
    const field = fields[index]
537 +
    if (field in from) {
538 +
      // @ts-expect-error: indexable.
539 +
      node[field] = from[field]
540 +
    }
541 +
  }
542 +
543 +
  return node
544 +
}

@@ -1,544 +1,8 @@
Loading
1 1
/**
2 -
 * @typedef {import('estree-jsx').Node} Node
3 -
 * @typedef {import('estree-jsx').Comment} Comment
4 -
 * @typedef {import('estree-jsx').Expression} Expression
5 -
 * @typedef {import('estree-jsx').Pattern} Pattern
6 -
 * @typedef {import('estree-jsx').ObjectExpression} ObjectExpression
7 -
 * @typedef {import('estree-jsx').Property} Property
8 -
 * @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
9 -
 * @typedef {import('estree-jsx').SpreadElement} SpreadElement
10 -
 * @typedef {import('estree-jsx').MemberExpression} MemberExpression
11 -
 * @typedef {import('estree-jsx').Literal} Literal
12 -
 * @typedef {import('estree-jsx').Identifier} Identifier
13 -
 * @typedef {import('estree-jsx').JSXElement} JSXElement
14 -
 * @typedef {import('estree-jsx').JSXFragment} JSXFragment
15 -
 * @typedef {import('estree-jsx').JSXText} JSXText
16 -
 * @typedef {import('estree-jsx').JSXExpressionContainer} JSXExpressionContainer
17 -
 * @typedef {import('estree-jsx').JSXEmptyExpression} JSXEmptyExpression
18 -
 * @typedef {import('estree-jsx').JSXSpreadChild} JSXSpreadChild
19 -
 * @typedef {import('estree-jsx').JSXAttribute} JSXAttribute
20 -
 * @typedef {import('estree-jsx').JSXSpreadAttribute} JSXSpreadAttribute
21 -
 * @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
22 -
 * @typedef {import('estree-jsx').JSXNamespacedName} JSXNamespacedName
23 -
 * @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
2 +
 * @typedef {import('./lib/index.js').Node} Node
3 +
 * @typedef {import('./lib/index.js').Options} Options
24 4
 *
25 -
 * @typedef {import('estree-walker').SyncHandler} SyncHandler
26 -
 *
27 -
 * @typedef BuildJsxOptions
28 -
 * @property {'automatic'|'classic'} [runtime='classic']
29 -
 * @property {string} [importSource='react']
30 -
 * @property {string} [pragma='React.createElement']
31 -
 * @property {string} [pragmaFrag='React.Fragment']
32 -
 * @property {boolean} [development=false]
33 -
 * @property {string} [filePath]
34 -
 *
35 -
 * @typedef Annotations
36 -
 * @property {'automatic'|'classic'} [jsxRuntime]
37 -
 * @property {string} [jsx]
38 -
 * @property {string} [jsxFrag]
39 -
 * @property {string} [jsxImportSource]
5 +
 * @typedef {Options} BuildJsxOptions
40 6
 */
41 7
42 -
import {walk} from 'estree-walker'
43 -
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
44 -
45 -
const regex = /@(jsx|jsxFrag|jsxImportSource|jsxRuntime)\s+(\S+)/g
46 -
47 -
/**
48 -
 * @template {Node} T
49 -
 * @param {T} tree
50 -
 * @param {BuildJsxOptions} [options={}]
51 -
 * @returns {T}
52 -
 */
53 -
export function buildJsx(tree, options = {}) {
54 -
  let automatic = options.runtime === 'automatic'
55 -
  /** @type {Annotations} */
56 -
  const annotations = {}
57 -
  /** @type {{fragment?: boolean, jsx?: boolean, jsxs?: boolean, jsxDEV?: boolean}} */
58 -
  const imports = {}
59 -
60 -
  walk(tree, {
61 -
    // @ts-expect-error: types are wrong.
62 -
    enter(/** @type {Node} */ node) {
63 -
      if (node.type === 'Program') {
64 -
        const comments = node.comments || []
65 -
        let index = -1
66 -
67 -
        while (++index < comments.length) {
68 -
          regex.lastIndex = 0
69 -
70 -
          let match = regex.exec(comments[index].value)
71 -
72 -
          while (match) {
73 -
            // @ts-expect-error: indexable.
74 -
            annotations[match[1]] = match[2]
75 -
            match = regex.exec(comments[index].value)
76 -
          }
77 -
        }
78 -
79 -
        if (annotations.jsxRuntime) {
80 -
          if (annotations.jsxRuntime === 'automatic') {
81 -
            automatic = true
82 -
83 -
            if (annotations.jsx) {
84 -
              throw new Error('Unexpected `@jsx` pragma w/ automatic runtime')
85 -
            }
86 -
87 -
            if (annotations.jsxFrag) {
88 -
              throw new Error(
89 -
                'Unexpected `@jsxFrag` pragma w/ automatic runtime'
90 -
              )
91 -
            }
92 -
          } else if (annotations.jsxRuntime === 'classic') {
93 -
            automatic = false
94 -
95 -
            if (annotations.jsxImportSource) {
96 -
              throw new Error(
97 -
                'Unexpected `@jsxImportSource` w/ classic runtime'
98 -
              )
99 -
            }
100 -
          } else {
101 -
            throw new Error(
102 -
              'Unexpected `jsxRuntime` `' +
103 -
                annotations.jsxRuntime +
104 -
                '`, expected `automatic` or `classic`'
105 -
            )
106 -
          }
107 -
        }
108 -
      }
109 -
    },
110 -
    // @ts-expect-error: types are wrong.
111 -
    // eslint-disable-next-line complexity
112 -
    leave(/** @type {Node} */ node) {
113 -
      if (node.type === 'Program') {
114 -
        /** @type {Array<ImportSpecifier>} */
115 -
        const specifiers = []
116 -
117 -
        if (imports.fragment) {
118 -
          specifiers.push({
119 -
            type: 'ImportSpecifier',
120 -
            imported: {type: 'Identifier', name: 'Fragment'},
121 -
            local: {type: 'Identifier', name: '_Fragment'}
122 -
          })
123 -
        }
124 -
125 -
        if (imports.jsx) {
126 -
          specifiers.push({
127 -
            type: 'ImportSpecifier',
128 -
            imported: {type: 'Identifier', name: 'jsx'},
129 -
            local: {type: 'Identifier', name: '_jsx'}
130 -
          })
131 -
        }
132 -
133 -
        if (imports.jsxs) {
134 -
          specifiers.push({
135 -
            type: 'ImportSpecifier',
136 -
            imported: {type: 'Identifier', name: 'jsxs'},
137 -
            local: {type: 'Identifier', name: '_jsxs'}
138 -
          })
139 -
        }
140 -
141 -
        if (imports.jsxDEV) {
142 -
          specifiers.push({
143 -
            type: 'ImportSpecifier',
144 -
            imported: {type: 'Identifier', name: 'jsxDEV'},
145 -
            local: {type: 'Identifier', name: '_jsxDEV'}
146 -
          })
147 -
        }
148 -
149 -
        if (specifiers.length > 0) {
150 -
          node.body.unshift({
151 -
            type: 'ImportDeclaration',
152 -
            specifiers,
153 -
            source: {
154 -
              type: 'Literal',
155 -
              value:
156 -
                (annotations.jsxImportSource ||
157 -
                  options.importSource ||
158 -
                  'react') +
159 -
                (options.development ? '/jsx-dev-runtime' : '/jsx-runtime')
160 -
            }
161 -
          })
162 -
        }
163 -
      }
164 -
165 -
      if (node.type !== 'JSXElement' && node.type !== 'JSXFragment') {
166 -
        return
167 -
      }
168 -
169 -
      /** @type {Array<Expression>} */
170 -
      const children = []
171 -
      let index = -1
172 -
173 -
      // Figure out `children`.
174 -
      while (++index < node.children.length) {
175 -
        const child = node.children[index]
176 -
177 -
        if (child.type === 'JSXExpressionContainer') {
178 -
          // Ignore empty expressions.
179 -
          if (child.expression.type !== 'JSXEmptyExpression') {
180 -
            children.push(child.expression)
181 -
          }
182 -
        } else if (child.type === 'JSXText') {
183 -
          const value = child.value
184 -
            // Replace tabs w/ spaces.
185 -
            .replace(/\t/g, ' ')
186 -
            // Use line feeds, drop spaces around them.
187 -
            .replace(/ *(\r?\n|\r) */g, '\n')
188 -
            // Collapse multiple line feeds.
189 -
            .replace(/\n+/g, '\n')
190 -
            // Drop final line feeds.
191 -
            .replace(/\n+$/, '')
192 -
            // Replace line feeds with spaces.
193 -
            .replace(/\n/g, ' ')
194 -
195 -
          // Ignore collapsible text.
196 -
          if (value) {
197 -
            children.push(create(child, {type: 'Literal', value}))
198 -
          }
199 -
        } else {
200 -
          // @ts-expect-error JSX{Element,Fragment} have already been compiled,
201 -
          // and `JSXSpreadChild` is not supported in Babel either, so ignore
202 -
          // it.
203 -
          children.push(child)
204 -
        }
205 -
      }
206 -
207 -
      /** @type {MemberExpression|Literal|Identifier} */
208 -
      let name
209 -
      /** @type {Array<Property>} */
210 -
      let fields = []
211 -
      /** @type {Array<Expression>} */
212 -
      const objects = []
213 -
      /** @type {Array<Expression|SpreadElement>} */
214 -
      let parameters = []
215 -
      /** @type {Expression|undefined} */
216 -
      let key
217 -
218 -
      // Do the stuff needed for elements.
219 -
      if (node.type === 'JSXElement') {
220 -
        name = toIdentifier(node.openingElement.name)
221 -
222 -
        // If the name could be an identifier, but start with a lowercase letter,
223 -
        // it’s not a component.
224 -
        if (name.type === 'Identifier' && /^[a-z]/.test(name.name)) {
225 -
          name = create(name, {type: 'Literal', value: name.name})
226 -
        }
227 -
228 -
        /** @type {boolean|undefined} */
229 -
        let spread
230 -
        const attributes = node.openingElement.attributes
231 -
        let index = -1
232 -
233 -
        // Place props in the right order, because we might have duplicates
234 -
        // in them and what’s spread in.
235 -
        while (++index < attributes.length) {
236 -
          const attribute = attributes[index]
237 -
238 -
          if (attribute.type === 'JSXSpreadAttribute') {
239 -
            if (fields.length > 0) {
240 -
              objects.push({type: 'ObjectExpression', properties: fields})
241 -
              fields = []
242 -
            }
243 -
244 -
            objects.push(attribute.argument)
245 -
            spread = true
246 -
          } else {
247 -
            const prop = toProperty(attribute)
248 -
249 -
            if (
250 -
              automatic &&
251 -
              prop.key.type === 'Identifier' &&
252 -
              prop.key.name === 'key'
253 -
            ) {
254 -
              if (spread) {
255 -
                throw new Error(
256 -
                  'Expected `key` to come before any spread expressions'
257 -
                )
258 -
              }
259 -
260 -
              // @ts-expect-error I can’t see object patterns being used as
261 -
              // attribute values? 🤷‍♂️
262 -
              key = prop.value
263 -
            } else {
264 -
              fields.push(prop)
265 -
            }
266 -
          }
267 -
        }
268 -
      }
269 -
      // …and fragments.
270 -
      else if (automatic) {
271 -
        imports.fragment = true
272 -
        name = {type: 'Identifier', name: '_Fragment'}
273 -
      } else {
274 -
        name = toMemberExpression(
275 -
          annotations.jsxFrag || options.pragmaFrag || 'React.Fragment'
276 -
        )
277 -
      }
278 -
279 -
      if (automatic) {
280 -
        if (children.length > 0) {
281 -
          fields.push({
282 -
            type: 'Property',
283 -
            key: {type: 'Identifier', name: 'children'},
284 -
            value:
285 -
              children.length > 1
286 -
                ? {type: 'ArrayExpression', elements: children}
287 -
                : children[0],
288 -
            kind: 'init',
289 -
            method: false,
290 -
            shorthand: false,
291 -
            computed: false
292 -
          })
293 -
        }
294 -
      } else {
295 -
        parameters = children
296 -
      }
297 -
298 -
      if (fields.length > 0) {
299 -
        objects.push({type: 'ObjectExpression', properties: fields})
300 -
      }
301 -
302 -
      /** @type {Expression|undefined} */
303 -
      let props
304 -
      /** @type {MemberExpression|Literal|Identifier} */
305 -
      let callee
306 -
307 -
      if (objects.length > 1) {
308 -
        // Don’t mutate the first object, shallow clone instead.
309 -
        if (objects[0].type !== 'ObjectExpression') {
310 -
          objects.unshift({type: 'ObjectExpression', properties: []})
311 -
        }
312 -
313 -
        props = {
314 -
          type: 'CallExpression',
315 -
          callee: toMemberExpression('Object.assign'),
316 -
          arguments: objects,
317 -
          optional: false
318 -
        }
319 -
      } else if (objects.length > 0) {
320 -
        props = objects[0]
321 -
      }
322 -
323 -
      if (automatic) {
324 -
        parameters.push(props || {type: 'ObjectExpression', properties: []})
325 -
326 -
        if (key) {
327 -
          parameters.push(key)
328 -
        } else if (options.development) {
329 -
          parameters.push({type: 'Identifier', name: 'undefined'})
330 -
        }
331 -
332 -
        const isStaticChildren = children.length > 1
333 -
334 -
        if (options.development) {
335 -
          imports.jsxDEV = true
336 -
          callee = {
337 -
            type: 'Identifier',
338 -
            name: '_jsxDEV'
339 -
          }
340 -
          parameters.push({type: 'Literal', value: isStaticChildren})
341 -
342 -
          /** @type {ObjectExpression} */
343 -
          const source = {
344 -
            type: 'ObjectExpression',
345 -
            properties: [
346 -
              {
347 -
                type: 'Property',
348 -
                method: false,
349 -
                shorthand: false,
350 -
                computed: false,
351 -
                kind: 'init',
352 -
                key: {type: 'Identifier', name: 'fileName'},
353 -
                value: {
354 -
                  type: 'Literal',
355 -
                  value: options.filePath || '<source.js>'
356 -
                }
357 -
              }
358 -
            ]
359 -
          }
360 -
361 -
          if (node.loc) {
362 -
            source.properties.push(
363 -
              {
364 -
                type: 'Property',
365 -
                method: false,
366 -
                shorthand: false,
367 -
                computed: false,
368 -
                kind: 'init',
369 -
                key: {type: 'Identifier', name: 'lineNumber'},
370 -
                value: {type: 'Literal', value: node.loc.start.line}
371 -
              },
372 -
              {
373 -
                type: 'Property',
374 -
                method: false,
375 -
                shorthand: false,
376 -
                computed: false,
377 -
                kind: 'init',
378 -
                key: {type: 'Identifier', name: 'columnNumber'},
379 -
                value: {type: 'Literal', value: node.loc.start.column + 1}
380 -
              }
381 -
            )
382 -
          }
383 -
384 -
          parameters.push(source, {type: 'ThisExpression'})
385 -
        } else if (isStaticChildren) {
386 -
          imports.jsxs = true
387 -
          callee = {type: 'Identifier', name: '_jsxs'}
388 -
        } else {
389 -
          imports.jsx = true
390 -
          callee = {type: 'Identifier', name: '_jsx'}
391 -
        }
392 -
      }
393 -
      // Classic.
394 -
      else {
395 -
        // There are props or children.
396 -
        if (props || parameters.length > 0) {
397 -
          parameters.unshift(props || {type: 'Literal', value: null})
398 -
        }
399 -
400 -
        callee = toMemberExpression(
401 -
          annotations.jsx || options.pragma || 'React.createElement'
402 -
        )
403 -
      }
404 -
405 -
      parameters.unshift(name)
406 -
407 -
      this.replace(
408 -
        create(node, {
409 -
          type: 'CallExpression',
410 -
          callee,
411 -
          arguments: parameters,
412 -
          optional: false
413 -
        })
414 -
      )
415 -
    }
416 -
  })
417 -
418 -
  return tree
419 -
}
420 -
421 -
/**
422 -
 * @param {JSXAttribute} node
423 -
 * @returns {Property}
424 -
 */
425 -
function toProperty(node) {
426 -
  /** @type {Expression} */
427 -
  let value
428 -
429 -
  if (node.value) {
430 -
    if (node.value.type === 'JSXExpressionContainer') {
431 -
      // @ts-expect-error `JSXEmptyExpression` is not allowed in props.
432 -
      value = node.value.expression
433 -
    }
434 -
    // Literal or call expression.
435 -
    else {
436 -
      // @ts-expect-error: JSX{Element,Fragment} are already compiled to
437 -
      // `CallExpression`.
438 -
      value = node.value
439 -
      // @ts-expect-error Remove `raw` so we don’t get character references in
440 -
      // strings.
441 -
      delete value.raw
442 -
    }
443 -
  }
444 -
  // Boolean prop.
445 -
  else {
446 -
    value = {type: 'Literal', value: true}
447 -
  }
448 -
449 -
  return create(node, {
450 -
    type: 'Property',
451 -
    key: toIdentifier(node.name),
452 -
    value,
453 -
    kind: 'init',
454 -
    method: false,
455 -
    shorthand: false,
456 -
    computed: false
457 -
  })
458 -
}
459 -
460 -
/**
461 -
 * @param {JSXMemberExpression|JSXNamespacedName|JSXIdentifier} node
462 -
 * @returns {MemberExpression|Identifier|Literal}
463 -
 */
464 -
function toIdentifier(node) {
465 -
  /** @type {MemberExpression|Identifier|Literal} */
466 -
  let replace
467 -
468 -
  if (node.type === 'JSXMemberExpression') {
469 -
    // `property` is always a `JSXIdentifier`, but it could be something that
470 -
    // isn’t an ES identifier name.
471 -
    const id = toIdentifier(node.property)
472 -
    replace = {
473 -
      type: 'MemberExpression',
474 -
      object: toIdentifier(node.object),
475 -
      property: id,
476 -
      computed: id.type === 'Literal',
477 -
      optional: false
478 -
    }
479 -
  } else if (node.type === 'JSXNamespacedName') {
480 -
    replace = {
481 -
      type: 'Literal',
482 -
      value: node.namespace.name + ':' + node.name.name
483 -
    }
484 -
  }
485 -
  // Must be `JSXIdentifier`.
486 -
  else {
487 -
    replace = isIdentifierName(node.name)
488 -
      ? {type: 'Identifier', name: node.name}
489 -
      : {type: 'Literal', value: node.name}
490 -
  }
491 -
492 -
  return create(node, replace)
493 -
}
494 -
495 -
/**
496 -
 * @param {string} id
497 -
 * @returns {Identifier|Literal|MemberExpression}
498 -
 */
499 -
function toMemberExpression(id) {
500 -
  const identifiers = id.split('.')
501 -
  let index = -1
502 -
  /** @type {Identifier|Literal|MemberExpression|undefined} */
503 -
  let result
504 -
505 -
  while (++index < identifiers.length) {
506 -
    /** @type {Identifier|Literal} */
507 -
    const prop = isIdentifierName(identifiers[index])
508 -
      ? {type: 'Identifier', name: identifiers[index]}
509 -
      : {type: 'Literal', value: identifiers[index]}
510 -
    result = result
511 -
      ? {
512 -
          type: 'MemberExpression',
513 -
          object: result,
514 -
          property: prop,
515 -
          computed: Boolean(index && prop.type === 'Literal'),
516 -
          optional: false
517 -
        }
518 -
      : prop
519 -
  }
520 -
521 -
  // @ts-expect-error: always a result.
522 -
  return result
523 -
}
524 -
525 -
/**
526 -
 * @template {Node} T
527 -
 * @param {Node} from
528 -
 * @param {T} node
529 -
 * @returns {T}
530 -
 */
531 -
function create(from, node) {
532 -
  const fields = ['start', 'end', 'loc', 'range', 'comments']
533 -
  let index = -1
534 -
535 -
  while (++index < fields.length) {
536 -
    const field = fields[index]
537 -
    if (field in from) {
538 -
      // @ts-expect-error: indexable.
539 -
      node[field] = from[field]
540 -
    }
541 -
  }
542 -
543 -
  return node
544 -
}
8 +
export {buildJsx} from './lib/index.js'
Files Coverage
index.js 100.00%
lib/index.js 100.00%
Project Totals (2 files) 100.00%

No yaml found.

Create your codecov.yml to customize your Codecov experience

Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading