Skip to content

wickedev/regrafter

Repository files navigation

Regrafter

npm version npm downloads codecov License: MIT TypeScript Node.js

Programmatic AST transformation library for React/JSX code transformations with automatic dependency management.

Regrafter provides three core APIs for transforming React code:

  • move() - Relocate JSX elements within and across files
  • extract() - Extract JSX into reusable components
  • inline() - Inline components at their call sites

All transformations automatically manage dependencies (hooks, variables, imports, props, context, refs) to ensure code correctness.

Why Regrafter?

  • Safety First: Transformed code always compiles and maintains semantic correctness
  • Fully Automated: Dependencies are analyzed and resolved automatically—no manual tracking needed
  • Type-Safe: Built with TypeScript, returns Result<T, E> instead of throwing exceptions
  • Developer-Friendly: Rich error messages with automatic recovery suggestions

Features

  • Safe Transformations: Move, extract, and inline JSX with dependency management
  • Automatic Dependency Analysis: Tracks hooks, variables, imports, props, context, and refs
  • Smart Hoisting: Automatically hoists dependencies to valid scopes following React rules
  • Cross-File Operations: Transform code across multiple files with import/export management
  • Optimization: Sink over-hoisted dependencies back to minimal scopes
  • Validation: Check operations before execution with canMove(), canExtract()
  • Dry-Run Mode: Preview transformations without modifying files
  • Error Recovery: Structured errors with suggested fixes

Installation

npm install regrafter

Requirements:

  • Node.js ≥18
  • TypeScript ≥4.7.0 (optional peer dependency)

Quick Start

Move API

Relocate JSX elements with automatic dependency management:

import { move, Move, isOk } from "regrafter";

const files = [
  {
    path: "App.tsx",
    content: `
      import { useState } from 'react';
      import { Header } from './components/Header';
      import { Counter } from './components/Counter';

      function App() {
        const [count, setCount] = useState(0);
        return (
          <div>
            <Header />
            <Counter value={count} onChange={setCount} />
          </div>
        );
      }
    `,
  },
];

// Move <Counter /> inside <Header />
const result = move(
  files,
  { file: "App.tsx", line: 11, column: 13 }, // from: Counter
  { file: "App.tsx", line: 10, column: 13 }, // to: Header
  Move.Inside
);

if (isOk(result)) {
  console.log("Transformed:", result.value.codes[0].content);
  /* Dependencies (count, setCount) automatically hoisted and threaded

  Output example:

  Transformed:
  import { useState } from 'react';
  import { Header } from './components/Header';
  import { Counter } from './components/Counter';

  function App() {
    const [count, setCount] = useState(0);
    return (
      <div>
        <Header count={count} setCount={setCount}>
          <Counter value={count} onChange={setCount} />
        </Header>
      </div>
    );
  }
  */
}

Move Directions:

Regrafter supports four move directions:

  • Move.Before - Insert element before the target
  • Move.After - Insert element after the target
  • Move.Inside - Insert element as a child of the target (prepends to children by default)
  • Move.Replace - Replace the target with the element

Control insertion position with insertIndex:

When using Move.Inside, you can control where the element is inserted among the target's children:

// Insert at the beginning (default behavior)
move(files, from, to, Move.Inside);

// Insert at a specific position (0-based index)
move(files, from, to, Move.Inside, { insertIndex: 0 }); // First child
move(files, from, to, Move.Inside, { insertIndex: 2 }); // Third child

// Insert at the end (append)
move(files, from, to, Move.Inside, { insertIndex: -1 });

Note: By default, Move.Inside prepends (inserts at the beginning). Use insertIndex: -1 to append at the end.

Extract API

Extract JSX into a reusable component:

import { extract, isOk } from "regrafter";

const files = [
  {
    path: "App.tsx",
    content: `
    function App() {
      const userName = "John";
      const avatar = "/avatar.jpg";

      return (
        <div>
          <div className="profile">
            <img src={avatar} alt={userName} />
            <h2>{userName}</h2>
          </div>
        </div>
      );
    }
  `,
  },
];

const result = extract(
  files,
  { file: "App.tsx", line: 7, column: 11 }, // Select profile div to extract
  { componentName: "UserProfile" }
);

if (isOk(result)) {
  console.log("Created component:", result.value.component);
  console.log("Generated code:", result.value.codes[0].content);
  console.log("Stats:", result.value.stats);
  /* UserProfile component created with inferred props

  Output example:
  Created component: {
    name: 'UserProfile',
    file: 'App.tsx',
    props: [
      { name: 'userName', type: 'string', optional: false },
      { name: 'avatar', type: 'string', optional: false }
    ]
  }

  Generated code:
  function UserProfile({ userName, avatar }: UserProfileProps) {
    return (
      <div className="profile">
        <img src={avatar} alt={userName} />
        <h2>{userName}</h2>
      </div>
    );
  }

  function App() {
    const userName = "John";
    const avatar = "/avatar.jpg";
    return <UserProfile userName={userName} avatar={avatar} />;
  }

  Stats: {
    nodesExtracted: 5,
    dependenciesFound: 3,
    propsGenerated: 2
  }
  */
}

Inline API

Inline a component at its call sites:

import { inline, isOk } from "regrafter";

const files = [
  {
    path: "components.tsx",
    content: `
      export function Button({ onClick, children }) {
        return <button onClick={onClick}>{children}</button>;
      }
    `,
  },
  {
    path: "App.tsx",
    content: `
      import { Button } from './components';

      function App() {
        const handleClick = () => console.log('Submit');
        const handleCancel = () => console.log('Cancel');
        const handleReset = () => console.log('Reset');

        return (
          <div>
            <Button onClick={handleClick}>Submit</Button>
            <Button onClick={handleCancel}>Cancel</Button>
            <Button onClick={handleReset}>Reset</Button>
          </div>
        );
      }
    `,
  },
];

const result = inline(
  files,
  { file: "components.tsx", name: "Button" } // Component to inline
);

if (isOk(result)) {
  console.log("Result:", result.value);
  console.log(`Inlined ${result.value.inlinedCount} call sites`);

  /* Output example:
  Result: {
    codes: [
      {
        file: 'components.tsx',
        content: '// Button component removed',
        changed: true
      },
      {
        file: 'App.tsx',
        content: `
          function App() {
            const handleClick = () => console.log('Submit');
            const handleCancel = () => console.log('Cancel');
            const handleReset = () => console.log('Reset');

            return (
              <div>
                <button onClick={handleClick}>Submit</button>
                <button onClick={handleCancel}>Cancel</button>
                <button onClick={handleReset}>Reset</button>
              </div>
            );
          }
        `,
        changed: true
      }
    ],
    inlinedCount: 3
  }
  Inlined 3 call sites
  */
}

Core APIs

move()

Relocate JSX elements with automatic dependency management.

move(files, from, to, mode, options?): Result<TransformedCode[], RegraffError>

See: Move API Documentation

extract()

Extract JSX into a reusable component with automatic prop inference.

extract(files, selection, componentName, options?): Result<TransformedCode[], RegraffError>

See: Extract API Documentation

inline()

Inline a component at its call sites.

inline(files, component, options?): Result<InlineResult, RegraffError>

See: Inline API Documentation

Validation & Analysis

canMove(files, from, to, mode): boolean
analyze(files, from, to, mode): Result<MoveAnalysis, RegraffError>
canExtract(files, selection): boolean
analyzeExtract(files, selection, name): Result<ExtractAnalysis, RegraffError>

See: Validation APIs

Optimization

optimize(files, options?): Result<TransformedCode[], RegraffError>

See: Optimization API

Key Concepts

Selectors

Select elements by position or AST path:

// Position (line/column) - IDE-friendly
{ file: 'App.tsx', line: 10, column: 5 }

// AST path - Programmatic control
{ file: 'App.tsx', path: 'Program.body[0].declaration' }

Move Modes

enum Move {
  Inside = "inside", // Insert as child
  Before = "before", // Insert before target
  After = "after", // Insert after target
}

Result Pattern

All APIs return Result<T, E> instead of throwing:

import { move, isOk, isErr } from "regrafter";

const result = move(files, from, to, Move.Inside);

if (isOk(result)) {
  console.log("Success:", result.value);
} else {
  console.error("Error:", result.error);
}

See: Error Handling Guide

Dependency Management

Regrafter automatically tracks and resolves six types of dependencies:

  • Hook - React hooks (useState, useEffect, etc.) → Hoist to valid component
  • Variable - Local variables → Hoist or thread as props
  • Import - Module imports → Add/remove imports automatically
  • Prop - Component props → Thread through component tree
  • Context - React context → Hoist or thread values
  • Ref - React refs → Hoist or implement ref forwarding

See: Dependency Types Guide

Documentation

Development

# Install dependencies
npm install

# Run tests (TDD)
npm run test:watch

# Build
npm run build

# Lint & format
npm run lint:fix
npm run format

See: Contributing Guide for development guidelines

Performance

  • Single file (<1000 lines): <100ms
  • Multi-file (10 files): <500ms
  • Memory: <10x source file size

See: Architecture for technical details

License

MIT © wickedev

Links

About

A library for rearranging React elements through programmatic AST transformation.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 5

Languages