knitwork vs magicast vs recast: JavaScript Code Generation and Modification (2026)
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
| Feature | knitwork | magicast | recast |
|---|---|---|---|
| Purpose | Generate code strings | Modify existing code | AST code transforms |
| Approach | String concatenation | High-level proxy API | AST manipulation |
| Parsing needed | ❌ | ✅ (built-in) | ✅ |
| Preserves formatting | N/A | ✅ | ✅ |
| TypeScript support | N/A | ✅ | ✅ (with parser) |
| Config file helpers | ❌ | ✅ (Vite, Nuxt) | ❌ |
| Import generation | ✅ | ✅ | ✅ (AST builders) |
| Complex transforms | ❌ | ❌ | ✅ |
| Learning curve | Low | Low | High |
| Used by | Nuxt codegen | Nuxt CLI, module tools | jscodeshift, codemods |
| Weekly downloads | ~2M | ~5M | ~15M |
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.