Skip to main content

Guide

knitwork vs magicast vs recast 2026

Compare knitwork, magicast, and recast for generating and modifying JavaScript code programmatically. AST manipulation, code generation, config file now.

·PkgPulse Team·
0

TL;DR

knitwork is the UnJS code string generation utility — simple functions to generate JavaScript code strings (ESM imports/exports, JSON, raw code), no AST needed, used by Nuxt for code generation. magicast is the UnJS code modification tool — programmatically modify JavaScript/TypeScript files while preserving formatting, uses recast under the hood, designed for config file modification. recast is the AST-preserving code transformer — parse JavaScript into an AST, modify it, and print it back preserving original formatting, used by jscodeshift and many codemods. In 2026: knitwork for simple code string generation, magicast for config file modification, recast for complex AST-level code transforms.

Key Takeaways

  • knitwork: ~2M weekly downloads — UnJS, string-based code gen, ESM/JSON helpers
  • magicast: ~5M weekly downloads — UnJS, modify JS files preserving formatting, config files
  • recast: ~15M weekly downloads — AST parser/printer, preserves formatting, codemods
  • knitwork generates code strings (no parsing); magicast and recast modify existing code
  • magicast provides a higher-level API than recast for common operations
  • recast is the most powerful for complex AST transformations

knitwork

knitwork — code string generation:

ESM code generation

import {
  genImport, genExport, genDynamicImport,
  genObjectFromRaw, genArrayFromRaw,
  genString, genSafeVariableName,
} from "knitwork"

// Generate import statements:
genImport("express", "express")
// → import express from "express"

genImport("react", ["useState", "useEffect"])
// → import { useState, useEffect } from "react"

genImport("vue", { name: "ref", as: "vueRef" })
// → import { ref as vueRef } from "vue"

genImport("./styles.css")
// → import "./styles.css"

// Generate export statements:
genExport("./utils", ["formatDate", "parseURL"])
// → export { formatDate, parseURL } from "./utils"

genExport("express", "default")
// → export { default } from "express"

// Dynamic imports:
genDynamicImport("./heavy-module.js")
// → import("./heavy-module.js")

Object and array generation

import { genObjectFromRaw, genArrayFromRaw } from "knitwork"

// Generate object (values are raw code, not strings):
genObjectFromRaw({
  name: '"pkgpulse"',
  version: '"1.0.0"',
  port: "3000",
  isDev: "process.env.NODE_ENV === 'development'",
  handler: "() => console.log('ready')",
})
// → {
// →   name: "pkgpulse",
// →   version: "1.0.0",
// →   port: 3000,
// →   isDev: process.env.NODE_ENV === 'development',
// →   handler: () => console.log('ready')
// → }

// Generate array:
genArrayFromRaw(['"a"', '"b"', "myVar", "fn()"])
// → ["a", "b", myVar, fn()]

How Nuxt uses knitwork

import { genImport, genExport, genObjectFromRaw } from "knitwork"

// Nuxt generates virtual modules at build time:
function generatePluginsModule(plugins: string[]) {
  const imports = plugins.map((p, i) =>
    genImport(p, `plugin_${i}`)
  ).join("\n")

  const exports = genExport(undefined, {
    default: genArrayFromRaw(
      plugins.map((_, i) => `plugin_${i}`)
    ),
  })

  return `${imports}\n\n${exports}`
}

// Output:
// import plugin_0 from "~/plugins/auth"
// import plugin_1 from "~/plugins/analytics"
//
// export default [plugin_0, plugin_1]

Safe variable names

import { genSafeVariableName, genString } from "knitwork"

// Convert any string to a safe JS variable name:
genSafeVariableName("my-package")    // → "myPackage"
genSafeVariableName("@scope/pkg")    // → "scopePkg"
genSafeVariableName("123-invalid")   // → "_123Invalid"
genSafeVariableName("hello world")   // → "helloWorld"

// Safely generate a string literal:
genString('He said "hello"')
// → "He said \"hello\""

genString("Line 1\nLine 2")
// → "Line 1\\nLine 2"

magicast

magicast — code modification:

Modify config files

import { parseModule, generateCode } from "magicast"

// Parse existing config file:
const mod = parseModule(`
  export default {
    server: {
      port: 3000,
      host: "localhost",
    },
    plugins: ["react"],
  }
`)

// Modify values:
mod.exports.default.server.port = 8080
mod.exports.default.server.host = "0.0.0.0"

// Add new properties:
mod.exports.default.server.https = true

// Modify arrays:
mod.exports.default.plugins.push("vue")

// Generate updated code (preserves formatting!):
const { code } = generateCode(mod)
console.log(code)
// → export default {
// →   server: {
// →     port: 8080,
// →     host: "0.0.0.0",
// →     https: true,
// →   },
// →   plugins: ["react", "vue"],
// → }

Add imports

import { parseModule, generateCode } from "magicast"
import { addVitePlugin } from "magicast/helpers"

const mod = parseModule(`
  import { defineConfig } from "vite"
  import react from "@vitejs/plugin-react"

  export default defineConfig({
    plugins: [react()],
  })
`)

// Add a new Vite plugin:
addVitePlugin(mod, {
  from: "vite-plugin-pwa",
  imported: "VitePWA",
  constructor: "VitePWA",
  options: {
    registerType: "autoUpdate",
  },
})

const { code } = generateCode(mod)
// → import { defineConfig } from "vite"
// → import react from "@vitejs/plugin-react"
// → import { VitePWA } from "vite-plugin-pwa"
// →
// → export default defineConfig({
// →   plugins: [react(), VitePWA({ registerType: "autoUpdate" })],
// → })

Nuxt config modification

import { parseModule, generateCode } from "magicast"
import { addNuxtModule } from "magicast/helpers"

const mod = parseModule(`
  export default defineNuxtConfig({
    modules: ["@nuxt/content"],
  })
`)

// Add a Nuxt module:
addNuxtModule(mod, "@nuxt/image")
addNuxtModule(mod, "@pinia/nuxt")

const { code } = generateCode(mod)
// → export default defineNuxtConfig({
// →   modules: ["@nuxt/content", "@nuxt/image", "@pinia/nuxt"],
// → })

Modify package.json scripts

import { parseModule, generateCode } from "magicast"

// magicast can also handle JSON-like structures:
const mod = parseModule(`
  export default {
    scripts: {
      dev: "vite",
      build: "vite build",
    },
  }
`)

// Add a new script:
mod.exports.default.scripts.test = "vitest"
mod.exports.default.scripts.lint = "eslint ."

const { code } = generateCode(mod)

recast

recast — AST-preserving transformer:

Basic parsing and printing

import * as recast from "recast"

const code = `
// Important comment
const greeting = "Hello"
function sayHello(name) {
  return greeting + ", " + name + "!"
}
`

const ast = recast.parse(code)

// Print back — preserves original formatting:
const output = recast.print(ast)
console.log(output.code)
// → Identical to input (comments, whitespace preserved)

AST manipulation

import * as recast from "recast"
const b = recast.types.builders

const code = 'const x = 1 + 2'
const ast = recast.parse(code)

// Find and replace nodes:
recast.visit(ast, {
  visitBinaryExpression(path) {
    const { left, right, operator } = path.node
    if (
      operator === "+" &&
      left.type === "Literal" &&
      right.type === "Literal"
    ) {
      // Replace 1 + 2 with 3:
      path.replace(b.literal(left.value + right.value))
    }
    return false
  },
})

console.log(recast.print(ast).code)
// → const x = 3

Add imports

import * as recast from "recast"
const b = recast.types.builders

const code = `
import express from "express"

const app = express()
`

const ast = recast.parse(code)

// Create new import: import cors from "cors"
const newImport = b.importDeclaration(
  [b.importDefaultSpecifier(b.identifier("cors"))],
  b.literal("cors")
)

// Insert after first import:
ast.program.body.splice(1, 0, newImport)

console.log(recast.print(ast).code)
// → import express from "express"
// → import cors from "cors"
// →
// → const app = express()

Transform with TypeScript

import * as recast from "recast"
import * as tsParser from "recast/parsers/typescript"

const code = `
interface User {
  name: string
  email: string
}

function greet(user: User): string {
  return "Hello, " + user.name
}
`

// Parse with TypeScript support:
const ast = recast.parse(code, { parser: tsParser })

// Find all interface declarations:
recast.visit(ast, {
  visitTSInterfaceDeclaration(path) {
    console.log("Found interface:", path.node.id.name)
    // → "Found interface: User"
    this.traverse(path)
  },
})

// Modify and print (formatting preserved):
const output = recast.print(ast)

Building codemods

import * as recast from "recast"
const b = recast.types.builders

// Codemod: Convert var to const/let
function varToConstLet(code: string): string {
  const ast = recast.parse(code)

  recast.visit(ast, {
    visitVariableDeclaration(path) {
      if (path.node.kind === "var") {
        // Check if variable is reassigned:
        const name = path.node.declarations[0].id.name
        const isReassigned = hasReassignment(path.scope, name)
        path.node.kind = isReassigned ? "let" : "const"
      }
      this.traverse(path)
    },
  })

  return recast.print(ast).code
}

// Input:  var x = 1; var y = 2; y = 3;
// Output: const x = 1; let y = 2; y = 3;

Feature Comparison

Featureknitworkmagicastrecast
PurposeGenerate code stringsModify existing codeAST code transforms
ApproachString concatenationHigh-level proxy APIAST manipulation
Parsing needed✅ (built-in)
Preserves formattingN/A
TypeScript supportN/A✅ (with parser)
Config file helpers✅ (Vite, Nuxt)
Import generation✅ (AST builders)
Complex transforms
Learning curveLowLowHigh
Used byNuxt codegenNuxt CLI, module toolsjscodeshift, codemods
Weekly downloads~2M~5M~15M

Choosing the Right Level of Abstraction

The choice between these three tools reflects the level of abstraction appropriate for your code generation task. Knitwork operates at the string level — you compose code strings from well-defined helper functions, and the output is a string that gets written to a file. This is the right level for generating new files from scratch where you control the entire output. Magicast operates at the syntax tree level but exposes a JavaScript object proxy interface — you interact with the code as if it were a JavaScript object, and magicast translates those property assignments into AST mutations. This abstraction is appropriate for modifying configuration files with predictable structure. Recast operates at the raw AST level — you work directly with the AST nodes, traverse the tree with visitors, and build new nodes using builder functions. This is the right level for complex transformations that require understanding the full context of the code being modified.

Production Use Cases and Ecosystem Context

Knitwork is deeply embedded in the Nuxt ecosystem, used wherever the framework generates virtual modules, plugin registrations, and auto-import declarations at build time. Understanding knitwork matters if you are building Nuxt modules, because the string-based output is predictable and easy to test — generate the expected string, then assert equality. Magicast occupies a different role: it is the standard tool for Nuxt and Vite CLI tooling that modifies user configuration files during project setup or when adding integrations. The UnJS ecosystem chose magicast because it handles the common case of "add this plugin to the user's existing config" without destroying their comments or reformatting their file — a critical UX requirement for CLI tools that touch user-owned files.

Code Generation Quality and Correctness

String-based code generation with knitwork is safe for well-defined output templates but fragile for dynamic user input. If package names contain characters that are valid in npm but invalid in JavaScript identifiers (scoped packages like @scope/pkg), genSafeVariableName handles the transformation correctly. However, string interpolation for code values — rather than using genObjectFromRaw with explicit quoting — creates injection vulnerabilities if the input comes from user-controlled data. Always use knitwork's helper functions rather than template literals for code generation: genString(userInput) properly escapes quotes and newlines, while "\"" + userInput + "\"" does not.

AST Manipulation Complexity and Testing

Recast transforms are significantly harder to test than string-based generation because the test must assert on the printed output after AST manipulation, not the AST structure itself. Use recast's print to get the output string, then compare against a fixture file — snapshot testing works well here. The tricky edge case is formatting: recast preserves the original formatting of unmodified nodes, but newly inserted nodes use recast's default printer which may not match the surrounding style. For codemods that will run against diverse codebases, prefer inserting nodes that match the existing style by inspecting neighboring nodes' whitespace before building new ones with the AST builders.

TypeScript Support Across All Three

Magicast handles TypeScript configuration files correctly because it uses Babel with TypeScript preset for parsing — vite.config.ts and nuxt.config.ts files parse without needing separate TypeScript-specific handling. Recast's TypeScript parser (recast/parsers/typescript) uses @babel/parser in TypeScript mode, which supports all TypeScript syntax including decorators, abstract classes, and template literal types. Knitwork is TypeScript-aware only in the sense that it generates valid TypeScript code strings — it doesn't parse TypeScript and doesn't need to. When building a codemod with recast that targets TypeScript files, remember that type annotations are AST nodes — removing a TypeScript interface also requires removing any import declarations that only served that interface.

Migration Paths and Tooling Integration

jscodeshift wraps recast and adds parallel file processing, progress reporting, and dry-run support — for any codemod that will run across an entire repository, use jscodeshift rather than recast directly. jscodeshift's transform function receives the parsed AST from recast and expects the modified AST back, handling the print and write steps automatically. Magicast is the right choice for one-off configuration updates triggered by CLI commands, but for large-scale migrations across many config files, recast-based scripts are more robust because they provide the full AST visitor pattern for complex conditional logic. Neither tool handles binary files or files with syntax errors gracefully — always add error handling that falls back to reporting the problematic file rather than crashing the entire migration.

Handling Comment Preservation in Code Transforms

A common frustration with code transformation tools is that comments — including license headers, TODO annotations, and explanatory notes — are silently dropped during transformation. Recast's core value proposition is precisely this: because it preserves the original source tokens and only reprints nodes that were actually modified, comments attached to unmodified nodes are preserved verbatim. However, when you insert a new AST node adjacent to an existing node with a leading comment, recast may associate the comment with the new node rather than the original — test your transforms with comment-heavy source files to catch these edge cases. Magicast inherits recast's comment preservation for operations it proxies to recast, but higher-level operations like addPlugin() may generate new AST nodes without comments. Knitwork, operating on string generation rather than AST transformation, has no concept of comment preservation — you must explicitly include any comments in the generated code strings using template literals or the genComment() utility if available in your version.

When to Use Each

Use knitwork if:

  • Generating code strings from scratch (virtual modules, templates)
  • Need ESM import/export generation helpers
  • Building a framework that generates code files
  • Want the simplest approach (no AST, just strings)

Use magicast if:

  • Modifying config files programmatically (vite.config, nuxt.config)
  • Adding plugins, modules, or options to existing configs
  • Need to preserve original formatting and comments
  • Building CLI tools that modify user's config files

Use recast if:

  • Building codemods (automated code migrations)
  • Need complex AST-level transformations
  • Want to visit/modify specific node types
  • Building code analysis or transformation tools

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on knitwork v1.x, magicast v0.3.x, and recast v0.23.x.

Compare code generation and AST tooling on PkgPulse →

See also: AVA vs Jest and ohash vs object-hash vs hash-wasm, acorn vs @babel/parser vs espree.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.