npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

eslint-codemod-utils

v1.9.0

Published

A collection of AST helper functions for more complex ESLint rule fixes.

Downloads

17,235

Readme

ESLint Codemod Utilities

brach build status npm version The eslint-codemod-utils package is a library of helper functions designed to enable code evolution in a similar way to jscodeshift - but leaning on the live and ongoing enforcement of eslint in your source - rather than one off codemod scripts. It provides first class typescript support and will supercharge your custom eslint rules.

Installation

pnpm add -D eslint-codemod-utils
yarn add -D eslint-codemod-utils
npm i --save-dev eslint-codemod-utils

Getting started

To create a basic JSX node, you might do something like this:

import {
  jsxElement,
  jsxOpeningElement,
  jsxClosingElement,
  identifier,
} from 'eslint-codemod-utils'

const modalName = identifier({ name: 'Modal' })
const modal = jsxElement({
  openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }),
  closingElement: jsxClosingElement({ name: modalName }),
})

This would produce an espree compliant node type that you can also nicely stringify to apply your eslint fixes. For example:

modal.toString()
// produces: <Modal></Modal>

The real power of this approach is when combining these utilties with eslint rule custom fixe. In these cases, rather than relying on string manipulation - which can be inexact, hacky or complex to reason about - you can instead focus on only the fix you actually need to affect.

Your first eslint codemod

Writing a codemod is generally broken down into three parts:

  1. Find
  2. Modify
  3. Remove / Cleanup

The eslint custom rule API allows us to find nodes fairly simply, but how might we modify them? Let's say we're trying to add a new element required to be composed by our Design System's Modal element - a ModalBody which is going to be wrapped by the original Modal container. Assuming you've found the right node a normal fix might look like this:

import { Rule } from 'eslint'

function fix(fixer: Rule.RuleFixer) {
  return fixer.replaceText(node, '<Modal><ModalBody></ModalBody></Modal>')
}

So for this input:

const MyModal = () => <Modal></Modal>

We make this change:

- const MyModal = () => <Modal></Modal>
+ const MyModal = () => <Modal><ModalBody></ModalBody></Modal>

This kinda works, but the problem is the existing usage of Modal in our codebase is likely (guaranteed!) to be considerably more complex than this example.

  • If our Modal has props, we need to consider them
  • If our Modal has children, we need to consider them
  • If our Modal is aliased, we need to consider that

Instead of relying on string manipulation to reconstruct the existing AST, we instead leverage the information eslint is already giving to us.

import * as esUtils from 'eslint-codemod-utils'
import { Rule } from 'eslint'

// This is slightly more verbose, but it's considerably more robust -
// Simply re-using and spitting out the exisitng AST as a string
function fix(fixer: Rule.RuleFixer) {
  const jsxIdentifier = esUtils.jsxIdentifier({ name: 'ModalBody' })
  const modalBodyNode = esUtils.jsxElement({
    openingElement: esUtils.jsxOpeningElement({ name: jsxIdentifier }),
    closingElement: esUtils.jsxClosingElement({ name: jsxIdentifier }),
    // pass children of original element to new wrapper
    children: node.children,
  })
  return fixer.replaceText(
    node,
    esUtils.jsxElement({ ...node, children: [modalBodyNode] }).toString()
  )
}

The above will work for the original example:

- const MyModal = () => <Modal></Modal>
+ const MyModal = () => <Modal><ModalBody></ModalBody></Modal>

But it will also work for:

- const MyModal = () => <Modal type="full-width"></Modal>
+ const MyModal = () => <Modal type="full-width"><ModalBody></ModalBody></Modal>

Or:

- const MyModal = () => <Modal><SomeChild/></Modal>
+ const MyModal = () => <Modal><ModalBody><SomeChild/></ModalBody></Modal>

It's a declarative approach to solve the same problem.

See the eslint-plugin-example example for examples of more real world fixes.

How it works

The library provides a 1-1 mapping of types to utility functions every espree node type. These are all lowercase complements to the underlying type they represent; eg. jsxIdentifier produces a JSXIdentifier node representation. These nodes all implement their own toString. This means any string cast will recursively produce the correct string output for any valid espree AST.

Each helper takes in a valid espree node and spits out an augmented one that can be more easily stringified. See -> API for more.

Motivation

This idea came about after wrestling with the limitations of eslint rule fixes. For context, eslint rules rely heavily on string based utilities to apply fixes to code. For example this fix which appends a semi-colon to a Literal (from the eslint documentation website itself):

context.report({
  node: node,
  message: 'Missing semicolon',
  fix: function (fixer) {
    return fixer.insertTextAfter(node, ';')
  },
})

This works fine if your fixes are trivial, but it works less well for more complex uses cases. As soon as you need to traverse other AST nodes and combine information for a fix, combine fixes; the simplicity of the RuleFixer API starts to buckle.

In codemod tools like jscodeshift, the AST is baked in to the way fixes are applied - rather than applying fixes your script needs to return a collection of AST nodes which are then parsed and integrated into the source. This is a little more heavy duty but it also is more resillient.

The missing piece for ESlint is a matching set of utilties to allow the flexibility to dive into the AST approach where and when a developer feels it is appropriate. This library aims to bridge some of that gap and with some different thinking around just how powerful ESLint can be.

Fixes can then theoretically deal with more complex use cases like this:

/**
 * This is part of a fix to demonstrate changing a prop in a specific element with
 * a much more surgical approach to node manipulation.
 */
import {
  jsxOpeningElement,
  jsxAttribute,
  jsxIdentifier,
} from 'eslint-codemod-utils'

// ... further down the file
context.report({
  node: node,
  message: 'error',
  fix(fixer) {
    // The variables 'fixed' works with the espree AST to create
    // its own representation which can easily be stringified
    const fixed = jsxOpeningElement({
      name: node.name,
      selfClosing: node.selfClosing,
      attributes: node.attributes.map((attr) => {
        if (attr.type === 'JSXAttribute' && attr.name.name === 'open') {
          const internal = jsxAttribute({
            // espree nodes are spread into the util with no issues
            ...attr,
            // others are recreated or re-mapped
            name: jsxIdentifier({
              ...attr.name,
              name: 'isOpen',
            }),
          })
          return internal
        }

        return attr
      }),
    })

    return fixer.replaceText(node, fixed.toString())
  },
})