yufka

Transform JavaScript ASTs the easy way

Usage no npm install needed!

<script type="module">
  import yufka from 'https://cdn.skypack.dev/yufka';
</script>

README


A rolled-up Yufka with stuffing

Yufka

Transform JavaScript ASTs the easy way.

Tests npm

Yufka aims to be the unofficial successor to Falafel and fixes most of its outstanding issues.

Some benefits over Falafel are:

However, Yufka is not a drop-in replacement for Falafel — see its list of differences.

Installation

npm install --save yufka

Motivation

Yufka is the ideal tool for programmatically making small & simple modifications to your JavaScript code. (For complex use cases, you probably want to head for more sophisticated solutions like @babel/traverse.)

As an introducing example, let's put a function wrapper around all array literals:

const yufka = require('yufka')

const source = `
const xs = [1, 2, [3, 4]]
const ys = [5, 6]
console.log([xs, ys])
`

const result = yufka(source, (node, { update, source }) => {
  if (node.type === 'ArrayExpression') {
    update(`fn(${source()})`)
  }
})

console.log(result.toString())

Output:

const xs = fn([1, 2, fn([3, 4])])
const ys = fn([5, 6])
console.log(fn([xs, ys]))

Usage

How it Works

function yufka(source, options = {}, manipulator)

Transform the string source with the function manipulator, returning an output object.

For every node in the AST, manipulator(node, helpers) fires. The recursive walk is an in-order traversal, so children get called before their parents. This makes it easier to write nested transforms since transforming parents often requires transforming their children first anyway.

The yufka() return value is an object with two properties:

Calling .toString() on a Yufka result object will return its source code.

Pro Tip: Don't know how a JavaScript AST looks like? Have a look at astexplorer.net to get an idea.

Options

All options are, as the name says, optional. If you want to provide an options object, its place is between the source code and the manipulator function.

Acorn Options

Any options for the underlying acorn parser can be passed to options.acorn:

yufka(source, { acorn: { sourceType: 'module' } }, (node, helpers) => {
  // Parse the `source` as an ES module
})

Custom Parser

You may pass a custom acorn parser as options.parser to use that instead of the default acorn version coming with this library:

const acorn = require('acorn')
const jsx = require('acorn-jsx')
const parser = acorn.Parser.extend(jsx())

yufka(source, { parser }, (node, helpers) => {
  // Parse the `source` as JSX
})

Source Maps

Yufka uses magic-string under the hood to generate source maps for your code modifications. You can pass its source map options as options.sourceMap:

const acorn = require('acorn')
const jsx = require('acorn-jsx')
const parser = acorn.Parser.extend(jsx())

yufka(source, { sourceMap: { hires: true } }, (node, helpers) => {
  // Create a high-resolution source map
})

Helpers

The helpers object passed to the manipulator function exposes the following methods. All of these methods handle the current AST node (the one that's passed to the manipulator as its first argument).

However, all of these methods take an AST node as an optional first parameter if you want to access other nodes.

Example:

yufka('x = 1', (node, { source }) => {
  if (node.type === 'AssignmentExpression') {
    // `node` refers to the `x = 1` Expression
    source()           // returns "x = 1"
    source(node.right) // returns "1"
  }
})

source()

Return the source code for the given node, including any modifications made to child nodes:

yufka('true', (node, { source, update }) => {
  if (node.type === 'Literal') {
    source() // returns "true"
    update('false')
    source() // returns "false"
  }
})

update(replacement)

Replace the source of the affected node with the replacement string:

const result = yufka('4 + 2', (node, { source, update }) => {
  if (node.type === 'BinaryExpression') {
    update(source(node.left) + source(node.right))
  }
})

console.log(result.toString())

Output:

42

parent(levels = 1)

From the starting node, climb up the syntax tree levels times. Getting an ancestor node of the program root yields undefined.

yufka('x = [1]', (node, { parent }) => {
  if (node.type === 'Literal') {
    // `node` refers to the `1` literal
    parent()  // same as parent(1), refers to the `[1]` expression
    parent(2) // refers to the `x = [1]` assignment expression
    parent(3) // refers to the `x = [1]` statement
    parent(4) // refers to the program as a whole (root node)
    parent(5) // yields `undefined`, same as parent(6), parent(7) etc.
  }
})

External Helper Access

Tip: If you want to extract manipulation behavior into standalone functions, you can access the helpers directly on the yufka instance (e.g. yufka.source()) where they are not bound to a specific node:

// Standalone function, increments node's value if it's a number
const increment = node => {
  if (node.type === 'Literal' && typeof node.value === 'number') {
    yufka.update(node, String(node.value + 1))
  }
}

const result = yufka('x = 1', node => {
  increment(node)
})

console.log(result.toString())

Output:

x = 2

Asynchronous Manipulations

The manipulator function may return a Promise. If it does, Yufka will wait for that to resolve, making the whole yufka() function return a Promise resolving to the result object (instead of returning the result object directly):

const got = require('got') // see www.npmjs.com/package/got

const source = `
const content = curl("https://example.com")
`
const deferredResult = yufka(source, async (node, { source, update }) => {
  if (node.type === 'CallExpression' && node.callee.name === 'curl') {
    // Replace all cUrl calls with their actual content

    // Get the URL (will only work for simple string literals)
    const url = node.arguments[0].value

    // Fetch the URL's contents
    const contents = (await got(url)).body

    // Replace the cUrl() call with the fetched contents
    update(JSON.stringify(contents))
  }
})

// Result is not available immediately, we need to await it
deferredResult.then(result => {
  console.log(result.toString())
})

Output:

const content = "<!doctype html>\n<html>\n[...]\n</html>"

Note: You have to return a promise if you want to commit updates asynchronously. Once the manipulator function is done running, any update() calls originating from it will throw an error.

Credit

  • While the source code of this package has virtually nothing left from the Falafel codebase, Yufka actually started out as a fork and its concepts stem from there.
  • Yufka's logo is based on a graphic from the Twemoji Project (licensed under CC BY 4.0).