esast-util-from-estree
Version:
esast utility to transform from estree
122 lines (104 loc) • 3.25 kB
JavaScript
/**
* @typedef {import('estree-jsx').Node} Nodes
*/
/**
* @typedef Options
* Configuration.
* @property {boolean | null | undefined} [dirty=false]
* Leave discouraged fields in the tree (default: `false`).
*/
/**
* @template T
* @template U
* @typedef {{[K in keyof T]: T[K] extends U ? K : never}[keyof T]} KeysOfType
*/
/**
* @template T
* @typedef {Exclude<KeysOfType<T, Exclude<T[keyof T], undefined>>, undefined>} RequiredKeys
*/
/**
* @template T
* @typedef {Exclude<keyof T, RequiredKeys<T>>} OptionalKeys
*/
import {visit} from 'estree-util-visit'
import {positionFromEstree} from 'unist-util-position-from-estree'
/** @type {Options} */
const emptyOptions = {}
/**
* Turn an estree into an esast.
*
* @template {Nodes} Kind
* Node kind.
* @param {Kind} estree
* estree.
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns {Kind}
* Clean clone of `estree`.
*/
export function fromEstree(estree, options) {
const settings = options || emptyOptions
/** @type {Kind} */
// Drop the `Node` and such constructors on Acorn nodes.
const esast = JSON.parse(JSON.stringify(estree, ignoreBigint))
visit(esast, {
leave(node) {
const position = positionFromEstree(node)
if (!settings.dirty) {
// Acorn specific.
// @ts-expect-error: acorn adds this.
if ('end' in node) remove(node, 'end')
// @ts-expect-error: acorn adds this.
if ('start' in node) remove(node, 'start')
if (node.type === 'JSXOpeningFragment') {
// @ts-expect-error: acorn adds this, but it should not exist.
if ('attributes' in node) remove(node, 'attributes')
// @ts-expect-error: acorn adds this, but it should not exist.
if ('selfClosing' in node) remove(node, 'selfClosing')
}
// Estree.
if ('loc' in node) remove(node, 'loc')
// @ts-expect-error: `JSXText` types are wrong: `raw` is optional.
if ('raw' in node) remove(node, 'raw')
if (node.type === 'Literal') {
// These `value`s on bigint/regex literals represent a raw value,
// which is an antipattern.
if ('bigint' in node) remove(node, 'value')
if ('regex' in node) remove(node, 'value')
}
}
if (node.type === 'Literal' && 'bigint' in node) {
const bigint = node.bigint
const match = /0[box]/.exec(bigint.slice(0, 2).toLowerCase())
if (match) {
const code = match[0].charCodeAt(1)
const base =
code === 98 /* `x` */ ? 2 : code === 111 /* `o` */ ? 8 : 16
node.bigint = Number.parseInt(bigint.slice(2), base).toString()
}
}
// @ts-expect-error: `position` is not in `Node`, but we add it anyway
// because it’s useful.
node.position = position
}
})
return esast
}
/**
* @template {Nodes} Kind
* @param {Kind} value
* @param {OptionalKeys<Kind>} key
* @returns {undefined}
*/
function remove(value, key) {
delete value[key]
}
/**
*
* @param {string} _
* @param {unknown} value
* @returns {unknown}
*/
function ignoreBigint(_, value) {
return typeof value === 'bigint' ? undefined : value
}