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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@puns/shiftkit

v0.0.4

Published

jscodeshift helper library for React/React Native codemods

Readme

ShiftKit

TypeScript-first helper library for building jscodeshift codemods for React and React Native.

Structure

| File | Purpose | |------|---------| | imports.ts | Import statement manipulation (add, remove, redirect) | | ast-builders.ts | AST node construction helpers (objects, arrays, members) | | token-helpers.ts | Design token member expression builder | | jsx/jsx-elements.ts | JSX element queries and creation | | jsx/jsx-attributes.ts | JSX attribute manipulation | | jsx/jsx-children.ts | JSX children manipulation | | jsx/jsx-clone.ts | JSX element cloning utilities | | jsx/jsx-conversions.ts | Type conversions (expressions ↔ literals ↔ attributes) | | jsx/jsx-transforms.ts | High-level JSX transformations | | jsx/extraction.ts | Value extraction with complexity detection | | rn/stylesheet.ts | React Native StyleSheet helpers |

Installation

npm install @puns/shiftkit jscodeshift

Basic Usage

import { addNamedImport } from '@puns/shiftkit'
import { findJSXElements, updateElementName } from '@puns/shiftkit/jsx'
import type { API, FileInfo } from 'jscodeshift'

export default function transform(fileInfo: FileInfo, api: API) {
  const j = api.jscodeshift
  const root = j(fileInfo.source)

  // Find and rename elements
  findJSXElements(root, 'Box', j).forEach(path => {
    updateElementName(path, 'View')
  })

  // Update imports
  addNamedImport(root, 'react-native', 'View', j)

  return root.toSource()
}

Design Principles

  • jQuery-inspired naming: intuitive method names (find*, add*, remove*, clone*)
  • Type-safe: strict TypeScript with jscodeshift types
  • Functional: stateless helpers, no classes
  • Explicit: always pass j (jscodeshift API) to functions
  • Safe: validates AST node types before operations

Import Management

Add Named Import

Adds import to existing statement or creates new one.

import { addNamedImport } from '@puns/shiftkit'

addNamedImport(root, 'react-native', 'View', j)
// Result: import { View } from 'react-native'

addNamedImport(root, 'react-native', 'Text', j)
// Result: import { View, Text } from 'react-native'

Remove Named Import

Removes import specifier or entire statement if last one.

import { removeNamedImport } from '@puns/shiftkit'

const imports = root.find(j.ImportDeclaration, { source: { value: 'old-lib' } })
removeNamedImport(imports, 'OldComponent', j)

Redirect Import

Changes import path while preserving type imports.

import { redirectImport, matchesImportPath } from '@puns/shiftkit'

root.find(j.ImportDeclaration)
  .filter(path => matchesImportPath('@old/lib')(path.node.source.value))
  .forEach(path => {
    j(path).replaceWith(redirectImport(path.node, '@new/lib', j))
  })

Check Import Exists

import { hasNamedImport } from '@puns/shiftkit'

const reactImports = root.find(j.ImportDeclaration, { source: { value: 'react' } })
if (hasNamedImport(reactImports, 'useState')) {
  // Transform logic
}

JSX Element Operations

Find Elements

import { findJSXElements, findJSXMemberElements } from '@puns/shiftkit/jsx'

// Simple elements
findJSXElements(root, 'Button', j).forEach(path => {
  // Transform each <Button />
})

// Member expressions
findJSXMemberElements(root, 'Alert', 'Title', j).forEach(path => {
  // Transform each <Alert.Title />
})

Create Elements

import {
  createSelfClosingElement,
  createElementWithChildren,
  createMemberElement
} from '@puns/shiftkit/jsx'

// Self-closing
const icon = createSelfClosingElement('Icon', [
  j.jsxAttribute(j.jsxIdentifier('name'), j.stringLiteral('check'))
], j)
// Result: <Icon name="check" />

// With children
const button = createElementWithChildren('Button', [], [
  j.jsxText('Click me')
], j)
// Result: <Button>Click me</Button>

// Member expression
const title = createMemberElement('Card', 'Title', [], [
  j.jsxText('Heading')
], j)
// Result: <Card.Title>Heading</Card.Title>

Element Utilities

import { getElementName, hasChildren, getNonEmptyChildren } from '@puns/shiftkit/jsx'

const name = getElementName(element)
// Returns: 'Button' or 'Alert.Title'

const hasKids = hasChildren(element)
// Returns: boolean

const kids = getNonEmptyChildren(element)
// Returns: children excluding whitespace-only text nodes

JSX Attributes

Find and Get Attributes

import {
  findAttribute,
  getAttribute,
  getAttributeValue,
  hasAttribute
} from '@puns/shiftkit/jsx'

const attrs = element.openingElement.attributes

// Find attribute node
const attr = findAttribute(attrs, 'variant')

// Get attribute value (wrapped)
const value = getAttribute(attrs, 'variant')
// Returns: JSXExpressionContainer or StringLiteral

// Get unwrapped value
const unwrapped = getAttributeValue(attrs, 'variant')
// Returns: Expression or Literal

// Check existence
if (hasAttribute(attrs, 'disabled')) {
  // Handle disabled state
}

Create Attributes

import {
  createAttribute,
  createStringAttribute,
  createObjectAttribute
} from '@puns/shiftkit/jsx'

// Auto-wrapped
const attr1 = createAttribute('size', 'large', j)
// Result: size="large"

const attr2 = createAttribute('count', j.numericLiteral(5), j)
// Result: count={5}

// String literal
const attr3 = createStringAttribute('variant', 'primary', j)
// Result: variant="primary"

// Object expression
const attr4 = createObjectAttribute('style', { flex: j.numericLiteral(1) }, j)
// Result: style={{ flex: 1 }}

Modify Attributes

import { setAttributes, addAttributes, removeAttributes } from '@puns/shiftkit/jsx'

// Replace all
setAttributes(element, [newAttr1, newAttr2])

// Add more
addAttributes(element, [newAttr3])

// Remove by name
removeAttributes(element.openingElement.attributes, ['disabled', 'hidden'])

Filter Attributes

import { filterAttributes } from '@puns/shiftkit/jsx'

// Allow list
const allowed = filterAttributes(attrs, { allow: ['className', 'style'] })

// Deny list
const filtered = filterAttributes(attrs, { deny: ['onClick', 'onPress'] })

// Both
const result = filterAttributes(attrs, {
  allow: ['className', 'id', 'style'],
  deny: ['style'] // className and id only
})

JSX Children

Add Children

import { addChildren } from '@puns/shiftkit/jsx'

addChildren(element, [newChild1, newChild2], 'end')
addChildren(element, [newChild], 'start')

Replace and Remove

import { replaceChildren, removeChildren } from '@puns/shiftkit/jsx'

replaceChildren(element, [newChild1, newChild2])

// Remove by predicate
const removed = removeChildren(element, child =>
  child.type === 'JSXElement' && getElementName(child) === 'Icon'
)

Wrap Children

import { wrapChildren } from '@puns/shiftkit/jsx'

wrapChildren(element, 'View', j)
// Before: <Parent><Child /></Parent>
// After:  <Parent><View><Child /></View></Parent>

Query Children

import {
  findChild,
  findChildByName,
  getChildrenCount,
  getTextContent
} from '@puns/shiftkit/jsx'

// Find by predicate
const child = findChild(element, c => c.type === 'JSXElement')

// Find by element name
const icon = findChildByName(element, 'Icon')

// Count non-whitespace children
const count = getChildrenCount(element)

// Extract all text
const text = getTextContent(element)
// Returns: 'Hello World' from <div>Hello <span>World</span></div>

Insert and Remove at Index

import { insertChildAt, removeChildAt } from '@puns/shiftkit/jsx'

insertChildAt(element, newChild, 2)
const removed = removeChildAt(element, 1)

JSX Cloning

Basic Cloning

import { cloneElement, cloneWithNewName } from '@puns/shiftkit/jsx'

// Deep clone
const copy = cloneElement(element, j)

// Clone with new name
const view = cloneWithNewName(buttonElement, 'View', j)
// <Button /> → <View />

Clone with Modifications

import {
  cloneWithAttributes,
  cloneWithAddedAttributes,
  cloneWithChildren,
  cloneAsSelfClosing
} from '@puns/shiftkit/jsx'

// Replace attributes
const el1 = cloneWithAttributes(element, [newAttr1, newAttr2], j)

// Add attributes
const el2 = cloneWithAddedAttributes(element, [extraAttr], j)

// Replace children
const el3 = cloneWithChildren(element, [newChild], j)

// Make self-closing
const el4 = cloneAsSelfClosing(element, j)
// <Button>Text</Button> → <Button />

Transform While Cloning

import {
  cloneWithTransformedAttributes,
  cloneWithTransformedChildren
} from '@puns/shiftkit/jsx'

// Transform attributes
const el1 = cloneWithTransformedAttributes(element, attrs => {
  return attrs.filter(attr => attr.name.name !== 'deprecated')
}, j)

// Transform children
const el2 = cloneWithTransformedChildren(element, children => {
  return children.map(child => /* transform */)
}, j)

Type Conversions

Expression Conversions

import {
  stringToExpression,
  expressionToString,
  ensureExpression,
  ensureLiteral
} from '@puns/shiftkit/jsx'

// String to identifier expression
const expr = stringToExpression('myVar', j)
// Result: {myVar}

// Extract string from expression
const str = expressionToString(jsxExprContainer)
// Returns: 'value' or null

// Ensure proper wrapping
const wrapped = ensureExpression(value, j)
// Handles literals, expressions, primitives

// Ensure literal node
const literal = ensureLiteral(42, j)
// Result: NumericLiteral

Attribute Value Conversions

import { toAttributeValue, fromAttributeValue } from '@puns/shiftkit/jsx'

// To JSX attribute format
const attrVal1 = toAttributeValue('text', j)
// Result: StringLiteral

const attrVal2 = toAttributeValue(42, j)
// Result: JSXExpressionContainer(NumericLiteral(42))

// Extract raw value
const raw = fromAttributeValue(attrValue)
// Returns: primitive value or AST node

Boolean Attributes

import { toBooleanAttribute } from '@puns/shiftkit/jsx'

const attr1 = toBooleanAttribute(true, j)
// Result: null (JSX uses <Component enabled />)

const attr2 = toBooleanAttribute(false, j)
// Result: JSXExpressionContainer(BooleanLiteral(false))

Style Objects

import { toStyleObject } from '@puns/shiftkit/jsx'

const styleObj = toStyleObject({ flex: 1, padding: 16 }, j)
// Result: ObjectExpression { flex: 1, padding: 16 }

Numeric Parsing

import { parseNumeric, stringToNumeric } from '@puns/shiftkit/jsx'

const num = parseNumeric('42')
// Returns: 42

const literal = stringToNumeric('42', j)
// Result: NumericLiteral(42)

High-Level Transforms

Update Element Name

import { updateElementName } from '@puns/shiftkit/jsx'

findJSXElements(root, 'Box', j).forEach(path => {
  updateElementName(path, 'View')
})
// <Box /> → <View />

Manage Props

import { removePropsFromElement, addPropsToElement } from '@puns/shiftkit/jsx'

const attrs = element.openingElement.attributes
const toRemove = attrs.filter(attr => attr.name.name === 'deprecated')

removePropsFromElement(attrs, toRemove)

addPropsToElement(attrs, {
  variant: j.stringLiteral('new'),
  size: j.numericLiteral(16)
}, j)

Style Prop Building

import { buildStyleValue, addStyleProp } from '@puns/shiftkit/jsx'

const elementStyles = []
const styleValue = buildStyleValue(
  { flex: j.numericLiteral(1) }, // StyleSheet props
  { margin: j.numericLiteral(8) }, // Inline props
  'container', // StyleSheet entry name
  elementStyles, // Accumulator
  j,
  [existingStyleRef] // Existing refs
)

addStyleProp(element.openingElement.attributes, styleValue, j)
// Result: style={[existingStyleRef, styles.container, { margin: 8 }]}

Create View Wrapper

import { createViewWrapper } from '@puns/shiftkit/jsx'

const wrapped = createViewWrapper(
  childElement,
  j.memberExpression(j.identifier('styles'), j.identifier('wrapper')),
  j
)
// Result: <View style={styles.wrapper}>{child}</View>

Value Extraction

Extract Prop from JSX Element

import { extractPropFromJSXElement } from '@puns/shiftkit/jsx'

const iconName = extractPropFromJSXElement(iconElement, 'Icon', 'name')
// From: <Icon name="check" color="blue" />
// Returns: 'check' (string literal value)

const dynamicValue = extractPropFromJSXElement(element, 'Button', 'onPress')
// From: <Button onPress={handlePress} />
// Returns: Identifier AST node for 'handlePress'

Extract Simple Child

Extracts single child with complexity detection.

import { extractSimpleChild } from '@puns/shiftkit/jsx'

const result = extractSimpleChild(element.children, j, {
  allowedExpressionTypes: ['Identifier', 'MemberExpression', 'CallExpression']
})

if (result.isComplex) {
  // Has multiple children or JSX elements
  console.log('Cannot extract, too complex')
} else if (result.value) {
  // Single simple value extracted
  const newAttr = j.jsxAttribute(j.jsxIdentifier('label'), result.value)
}

| Option | Type | Description | |--------|------|-------------| | allowedExpressionTypes | string[] | Allowed AST node types for expressions |

Default allowed types: ['Identifier', 'CallExpression', 'MemberExpression', 'StringLiteral']

AST Builders

Object Expression

import { createObjectExpression } from '@puns/shiftkit/jsx'

const obj = createObjectExpression({
  name: j.identifier('userName'),
  age: '25', // Auto-converted to StringLiteral
  active: true // Need to pass j.booleanLiteral(true) for proper handling
}, j)
// Result: { name: userName, age: '25', active: true }

Member Expression

import { createMemberExpression } from '@puns/shiftkit/jsx'

const member1 = createMemberExpression('styles.button', j)
const member2 = createMemberExpression(['styles', 'button'], j)
// Both result in: styles.button

const member3 = createMemberExpression('theme.colors.primary', j)
// Result: theme.colors.primary

Array Expression

import { createArrayExpression } from '@puns/shiftkit/jsx'

const arr = createArrayExpression(['a', 'b', j.identifier('c')], j)
// Result: ['a', 'b', c]

Object Property

import { createProperty } from '@puns/shiftkit/jsx'

const prop = createProperty('margin', 16, j)
// Result: margin: 16

Nested Object

Recursively handles nested structures.

import { createNestedObject } from '@puns/shiftkit/jsx'

const nested = createNestedObject({
  image: {
    source: {
      uri: j.identifier('imageUri')
    }
  }
}, j)
// Result: { image: { source: { uri: imageUri } } }

Template Literal

import { createTemplateLiteral } from '@puns/shiftkit/jsx'

const template = createTemplateLiteral([
  'Hello ',
  j.identifier('name'),
  '!'
], j)
// Result: `Hello ${name}!`

Design Tokens

Build Token Path

Handles numeric keys with bracket notation.

import { buildTokenPath } from '@puns/shiftkit'

const token1 = buildTokenPath(j, 'color', 'background.primary')
// Result: color.background.primary

const token2 = buildTokenPath(j, 'color', 'blue.500')
// Result: color.blue['500']

const token3 = buildTokenPath(j, 'space', 'md')
// Result: space.md

Rules for bracket notation:

  • Starts with digit: ['500']
  • Starts with dash: ['-1']
  • Not valid identifier: ['my-prop']

React Native StyleSheet

Extract to StyleSheet

import { shouldExtractToStyleSheet } from '@puns/shiftkit/rn'

const canExtract = shouldExtractToStyleSheet(value, isTokenHelper)
// Returns true for: literals, token helpers (if flag set)
// Returns false for: variables, function calls, user member expressions

Build StyleSheet Properties

import { buildStyleSheetProperties } from '@puns/shiftkit/rn'

const props = buildStyleSheetProperties({
  flex: j.numericLiteral(1),
  padding: j.numericLiteral(16)
}, j)
// Result: [Property, Property]

Add or Extend StyleSheet

Appends to existing StyleSheet.create() or creates new one.

import { addOrExtendStyleSheet } from '@puns/shiftkit/rn'

const elementStyles = [
  { name: 'container', styles: { flex: j.numericLiteral(1) } },
  { name: 'text', styles: { fontSize: j.numericLiteral(16) } }
]

addOrExtendStyleSheet(root, elementStyles, j)
// Result: const styles = StyleSheet.create({ container: {...}, text: {...} })
// Also adds: import { StyleSheet } from 'react-native'

Real-World Example: Migrate Component

import { addNamedImport, removeNamedImport } from '@puns/shiftkit'
import {
  findJSXElements,
  updateElementName,
  cloneWithTransformedAttributes
} from '@puns/shiftkit/jsx'
import type { API, FileInfo } from 'jscodeshift'

export default function transform(fileInfo: FileInfo, api: API) {
  const j = api.jscodeshift
  const root = j(fileInfo.source)

  // Find old component
  findJSXElements(root, 'OldButton', j).forEach(path => {
    // Transform attributes
    const newElement = cloneWithTransformedAttributes(path.node, attrs => {
      return attrs.map(attr => {
        if (attr.name.name === 'variant') {
          // Rename prop
          return j.jsxAttribute(j.jsxIdentifier('appearance'), attr.value)
        }
        return attr
      }).filter(attr => attr.name.name !== 'deprecated')
    }, j)

    // Update name
    path.node.openingElement.name.name = 'NewButton'
    if (path.node.closingElement) {
      path.node.closingElement.name.name = 'NewButton'
    }
    path.node.openingElement.attributes = newElement.openingElement.attributes
  })

  // Update imports
  const oldImports = root.find(j.ImportDeclaration, { source: { value: 'old-lib' } })
  removeNamedImport(oldImports, 'OldButton', j)
  addNamedImport(root, 'new-lib', 'NewButton', j)

  return root.toSource()
}

Testing

# Type check
npm run typecheck

# Build
npm run build

# Test specific codemod
npx jscodeshift -t my-transform.ts src/**/*.tsx --dry --print

Do's and Don'ts

Do:

  • Always pass j (jscodeshift API) to helper functions
  • Check node types before operations
  • Use --dry --print flags when testing transforms
  • Handle both JSXIdentifier and JSXMemberExpression for element names
  • Filter whitespace-only text nodes when counting children

Don't:

  • Don't mutate AST nodes directly without cloning
  • Don't assume attributes array exists (check || [])
  • Don't forget to update both opening and closing element names
  • Don't skip adding imports after element renames
  • Don't use bracket notation for valid identifier keys

Performance Notes

| Operation | Complexity | Notes | |-----------|------------|-------| | findJSXElements | O(n) | Scans entire AST | | addNamedImport | O(n) | Searches existing imports | | cloneElement | O(1) | Shallow clone | | extractSimpleChild | O(n) | Filters children array | | buildTokenPath | O(d) | d = token path depth |

Module Dependencies

  • jscodeshift (peer): AST manipulation
  • @types/jscodeshift (dev): Type definitions

No runtime dependencies.

License

MIT