npm package: @puns/shiftkit
Description: jscodeshift helper library for React/React Native codemods
Author: unpunnyfuns
Latest version: 0.0.4

# 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

```bash
npm install @puns/shiftkit jscodeshift
```

## Basic Usage

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

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

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

### Replace and Remove

```typescript
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

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

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

### Query Children

```typescript
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

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

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

## JSX Cloning

### Basic Cloning

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

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

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

### Numeric Parsing

```typescript
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

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

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

### Manage Props

```typescript
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

```typescript
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

```typescript
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

```typescript
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.

```typescript
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

```typescript
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

```typescript
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

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

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

### Object Property

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

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

### Nested Object

Recursively handles nested structures.

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

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

### Template Literal

```typescript
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.

```typescript
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

```typescript
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

```typescript
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.

```typescript
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

```typescript
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

```bash
# 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