Library / SDK
unjs/magicast avatar
unjs/magicast

unjs/magicast: editing JS and TS files like JSON

đź§€ Programmatically modify JavaScript and TypeScript source codes with a simplified, elegant and familiar syntax powered by recast and babel.

2,478 stars52 forksTypeScriptMIT

At a glance

What is it?
Magicast wraps recast and Babel in a JSON-shaped API for rewriting imports, exports and config objects in JavaScript and TypeScript files. It is a static-code tool, and its own README says the convention cannot cover every case.
Who is it for?
Adopt magicast when you are writing tooling that edits static-ish configuration files and you want to preserve the author's formatting instead of reprinting the file. Do not adopt it for runtime code transformation, for files whose shape you cannot predict, or as a general codemod engine; the README itself warns that the convention cannot cover all possible cases and recommends wrapping every operation in try/catch.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What magicast solves, and for whom

Most tools that rewrite a config file do one of two things. They parse the file into a plain object, mutate the object, and serialize it back, which loses comments, quote style, and key order. Or they hand you an abstract syntax tree and let you walk it, which is correct but verbose. Magicast sits between those options. Its README describes the goal as modifying a JS/TS file and writing it back "magically just like JSON", and the package description in package.json says the same in one line: "Modify a JS/TS file and write back magically just like JSON!"

The audience is people writing developer tooling rather than application code. If you are building a CLI that adds a plugin entry to a Vite config, a scaffolder that appends a module to a Nuxt config, or a setup script that flips a flag in a TypeScript config file, the object you care about is usually exported as a literal or wrapped in a single function call. Magicast gives you that object as something you can index into and mutate, then writes the file back. The README lists the intended surface area directly: manipulating imports and exports, manipulating arguments passed to a function call such as defineConfig(), and preserving the formatting style (quotes, tabs) from the original code.

It is not a general-purpose refactoring engine. The README is explicit that magicast "serves as a simple and maintainable interface to update static-ish JavaScript code", and that as JavaScript is a very dynamic language the convention cannot cover all possible cases. That sentence is the boundary of the tool, and it is worth reading before the examples.

How the proxy layer sits on top of recast and Babel

The README states the foundation plainly: magicast is built on top of the AST parsed by recast and Babel. package.json confirms the runtime dependencies are @babel/parser, @babel/types and source-map-js, with recast listed among the devDependencies. So the pipeline is: Babel parses the source into an AST, recast holds the original text alongside that AST so it can reprint only the parts that changed, and magicast wraps the result in a proxy that behaves like a plain object.

The wrapper is what makes the API short. When you load a module and reach for mod.exports.default.foo, you are not reading a real object; you are going through a proxy that maps property access onto AST nodes. Assigning to a property, pushing to an array, or setting a value that does not exist yet all translate into node edits, and generateCode then turns the mutated tree back into text plus a source map. The README shows the return shape: generateCode(mod) gives back an object with code and map.

Two escape hatches matter. The first is mod.exports.default.$ast, which the README documents as a way to get the AST directly and "do something with ast" when the proxy does not model what you need. The second is the $type and $args pair used to detect whether the default export is a bare object or a function call. The README's own example checks mod.exports.default.$type === "function-call" and then reads mod.exports.default.$args[0] to get at the options object inside defineConfig(...). That check is not optional decoration; without it you are indexing into a call expression as if it were a literal.

The package exposes four entry points through the exports map: the root, ./core, ./helpers and ./package.json. The split exists because the root module carries filesystem utilities. The README says to import from magicast/core in a browser or worker, where there is no filesystem to read from.

Installing magicast and editing a config file

The README gives the install commands for three package managers. All of them install it as a development dependency, which matches its role as a build-time and tooling-time library.

sh
npm install -D magicast

The README also lists yarn add --dev magicast and pnpm add -D magicast for the other two. After installing, the README shows the utilities you can import from the package root: builders, createNode, generateCode and parseModule.

The first real use is the load-mutate-write cycle. Given a config.js that contains an exported object with a foo array, the README's example loads the file, pushes a value onto that array, and writes it back. The API is deliberately small: loadFile returns a module proxy, you mutate through it, writeFile persists the change.

js
import { loadFile, writeFile } from "magicast";

const mod = await loadFile("config.js");

mod.exports.default.foo.push("b");

await writeFile(mod, "config.js");

After that runs, the README shows the file containing foo: ["a", "b"]. The surrounding formatting is the part to look at: the README's stated behaviour is that magicast preserves the formatting style from the original code, so the added entry should appear in the style the file already used rather than a reprint in a default style.

If you do not want to touch the filesystem, parseModule and generateCode work on strings. The README parses a literal export, ensures the foo property exists with ||= [], pushes and unshifts values, then calls generateCode to get code and map back. That variant is the one to reach for inside a worker or a browser bundle, imported from magicast/core.

Building function calls and the AST escape hatch

Two examples in the README cover cases where plain property assignment is not enough. The first is creating a call expression. The builders namespace exposes functionCall, and the README assigns its result to a property on the module proxy.

js
import { builders, generateCode, parseModule } from "magicast";

const mod = parseModule(`export default {}`);

mod.exports.default.list = builders.functionCall("create", [1, 2, 3]);

console.log(mod.generateCode());

The README says that prints export default { list: create([1, 2, 3]) }. Note that the example calls mod.generateCode() while the earlier example destructures generateCode(mod); both forms appear in the README, and the method form is the one used here.

The second case is the one that decides whether magicast fits your target file at all. Configs are frequently written as export default defineConfig({ ... }) rather than a bare object literal. The README handles both by branching on $type, and the snippet is worth copying rather than reinventing, because the branch is the difference between reading the options object and reading a call node.

js
const mod = parseModule(`export default defineConfig({ foo: 'bar' })`);

const options
  = mod.exports.default.$type === "function-call"
    ? mod.exports.default.$args[0]
    : mod.exports.default;

console.log(options.foo);

The README says that logs bar. When the proxy still does not express what you need, mod.exports.default.$ast hands you the underlying node so you can work on it directly. That is also where you leave magicast's guarantees behind: edits made at the AST level are your responsibility, not the library's.

Dynamic code, error handling, and when to pick something else

The most useful paragraph in the README is the one that limits the tool. It states that magicast's convention cannot cover all possible cases, that any option might throw depending on the input code, and that you should always wrap calls in try/catch, ideally with defensive coding on top. The README's own example does exactly that: it wraps loadFile, the mutation and writeFile in a try block and, in the catch, logs that config.js could not be updated and tells the user to update it manually.

That is an honest framing and it should shape how you deploy the library. If your tool runs unattended in CI against files it did not write, a throw is not a theoretical concern; the README says the chance exists for every option. The pattern the README models, catching and falling back to a manual instruction, is the one to copy. Anything computed at runtime, anything spread in from another module, anything re-exported through an intermediate file, and anything whose default export is chosen by a conditional are all outside what a static proxy can reason about.

A real alternative for the same job is jscodeshift, which the repository does not mention but which occupies the same slot: it also parses to an AST and reprints, but it asks you to write transforms against named node types and traverse them explicitly, and it is built around running codemods across many files in batch. The difference in approach is the interface, not the parsing. Magicast trades reach for brevity: you mutate what looks like an object and stop thinking about nodes, and in exchange you accept that the convention only covers static-ish shapes and that the proxy may throw on input it does not model. jscodeshift costs you more code per transform and gives you explicit control over every node you touch. If your transform has to handle arbitrary syntax, or you are rewriting thousands of files in one sweep, the explicit model is the safer one.

The second alternative is doing nothing. If the file you need to change is JSON, or you control its format and can rewrite it wholesale, a parse-and-stringify round trip is simpler than an AST tool and has no formatting to preserve.

Maintenance, releases, and the MIT licence

The repository is not archived, and the last push was on 2026-09-26, two days before this writing. Releases are frequent and small: v0.5.5 on 2026-09-11, v0.5.4 on 2026-07-31, v0.5.3 on 2026-05-14. All three are 0.5.x patch releases, which tells you the public API is still pre-1.0 and that a minor bump is where breaking changes would be expected. There is a CHANGELOG.md at the repository root and a renovate.json, so dependency updates appear to be automated.

The upgrade cost is concentrated in two places. First, pre-1.0 semver means you should pin the version your tooling builds against rather than floating on a caret range, and read CHANGELOG.md before moving. Second, the helpers entry point is explicitly provisional. The README says the high-level helpers are an experiment to make common tasks easier, that you import them from magicast/helpers, and that they might be moved to a separate package in the future. It names addNuxtModule, addVitePlugin and deepMergeObject as examples and points readers at the source in src/helpers and the tests in test/helpers for details, which is a signal that the helper surface is documented by code rather than by prose. Building a long-lived tool on those helpers means planning for an import path change.

On licensing, package.json declares MIT and the repository carries a LICENSE file. MIT is permissive, so the usual obligations are attribution and including the licence text when you redistribute. That is a statement about what the repository declares, not legal advice; if you vendor or redistribute the code, read the LICENSE file itself and take your own counsel.

Editorial conclusion

Adopt magicast when you are writing tooling that edits static-ish configuration files and you want to preserve the author's formatting instead of reprinting the file. Do not adopt it for runtime code transformation, for files whose shape you cannot predict, or as a general codemod engine; the README itself warns that the convention cannot cover all possible cases and recommends wrapping every operation in try/catch. Before committing to it, verify two things against your own target files: that loadFile resolves the export you expect, including the defineConfig wrapper case, and that writeFile leaves the surrounding formatting intact. Note that the package publishes only the dist directory and that the helpers entry point is documented as experimental and may move to a separate package, so pin the version you build against.

Frequently asked questions

How do I install and use magicast?

Install it as a development dependency with npm install -D magicast, or the equivalent yarn add --dev magicast or pnpm add -D magicast shown in the README. Then import loadFile and writeFile from the package root, mutate the module proxy, and write the file back.

What does magicast do?

It lets you modify a JavaScript or TypeScript file and write it back using a JSON-like API, so you can push to an array or set a property on an exported object instead of walking an AST. It is built on the AST parsed by recast and Babel and preserves the original formatting style, such as quotes and tabs.

What is magicast?

Magicast is a TypeScript library from the unjs organisation that provides a simplified interface for updating static-ish JavaScript code, including module imports and exports and the arguments passed to a function call like defineConfig(). It is published under the MIT licence.

What is a magicast alternative?

jscodeshift occupies the same slot, parsing to an AST and reprinting, but it asks you to write transforms against named node types and traverse them explicitly, and it is built around batch codemods. Magicast trades that reach for a shorter, object-like interface that only covers static-ish shapes.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. unjs/magicast on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/unjs-magicast.svg)](https://hysenlabs.com/projects/unjs-magicast)