Open-source project
Rich-Harris/magic-string avatar
Rich-Harris/magic-string

magic-string: surgical string edits with source maps

Manipulate strings like a wizard

2,777 stars129 forksTypeScriptMIT

At a glance

What is it?
magic-string is an ESM-only JavaScript library for making small edits to source code while generating a version 3 source map. It fits build tools that reorder or wrap code, and it is the wrong tool when you need to parse or refactor syntax.
Who is it for?
Adopt magic-string if you are writing a bundler, a transpiler or a code-wrapping step and your edits are positional rather than syntactic. Do not adopt it if you need to understand JavaScript structure, rename bindings or reprint an AST: it never parses your input, so it cannot tell you whether an index falls inside a string literal or a comment.
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 last received commits 8 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

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

Editorial analysis

The niche magic-string was built to fill

The README frames the problem directly: you have source code, you want to replace a few characters, wrap it with a header and footer, and still emit a source map at the end. The author names recast as the heavier option, one that builds an AST, lets you manipulate it, and reprints with comments and formatting preserved. That path is described as overkill for light edits, and it does not apply at all when the source is not JavaScript. magic-string takes the other road. It never parses anything. You hand it a string and a set of positional operations, and it tracks what moved so a map can be produced afterwards.

The audience is narrow and the README admits it: build-tool authors, transpiler writers, anyone inserting a banner or a polyfill into a file and still wanting stack traces to point at the original line. If your task is renaming a variable across a project, magic-string gives you no help, because it has no notion of what a variable is.

How positional edits and source maps actually work

A MagicString instance holds the original string plus a record of edits. The key rule appears in the usage example: character indices always refer to the original string. In the README example, after updating indices 0 to 8, a second call uses indices 11 to 13 and still targets the original text, not the already-modified output. That single rule explains most of the library's behaviour and most of its failure modes.

The methods split into two families. append and prepend work at the ends of the whole string. appendLeft, appendRight, prependLeft and prependRight insert at an index, and the left/right choice decides which side of a boundary the insertion belongs to. The README states the consequence plainly: content inserted with appendLeft at an index is moved along if a range ending at that index is later moved, while appendRight follows a range starting at that index. That is the mechanism that lets a bundler reorder modules and keep inserted code attached to the right module.

Mapping generation is separate. generateMap returns a version 3 sourcemap with optional file, source and includeContent fields, and the returned object carries two non-enumerable helpers: toString, equivalent to JSON.stringify, and toUrl, which returns a DataURI. The README shows the common pattern of appending a sourceMappingURL comment built from toUrl. The hires option controls granularity: false by default, one mapping per line; true for a mapping on every character; "boundary" for segmentation at word boundaries; and "experimental-range", which uses range mappings and, per the README, requires support in the source map consumer. addSourcemapLocation adds specific indices when hires is false.

Installing magic-string and making a first edit

The package installs from npm and is ESM-only, so it cannot be pulled in with require in a CommonJS file. The README gives the install command:

bash
npm i magic-string

In a browser, the README imports the built module from an ESM CDN rather than bundling it:

html
<script type="module">
	import MagicString from 'https://unpkg.com/magic-string/dist/index.mjs';
</script>

A first real use is the README's own example. Construct an instance, update a range by original indices, and read the result back with toString:

js
import MagicString from 'magic-string'

const s = new MagicString('problems = 99')

s.update(0, 8, 'answer')
s.toString() // 'answer = 99'

s.update(11, 13, '42')
s.toString() // 'answer = 42'

The reader should notice that the second update still uses indices into the original string, so 11 to 13 covers the original "99" even though the text before it has changed length.

To produce a map, call generateMap with the source filename, then write both files. The README's example names the output converted.js and converted.js.map:

js
const map = s.generateMap({
  source: 'source.js',
  file: 'converted.js.map',
  includeContent: true,
}) // generates a v3 sourcemap

A constructor options object accepts filename, indentExclusionRanges, ignoreList and offset. The offset property is the one to understand early: the README states it adjusts the incoming position for slice, update, overwrite, appendLeft, prependLeft, appendRight, prependRight, move, reset and remove. Setting s.offset = 6 on the string "hello world" makes s.slice() return "world". That is useful when a fragment starts partway through a file and you want indices to remain absolute.

Where magic-string breaks down

The library cannot validate your indices. Because it never parses the source, nothing stops you from updating a range that lands inside a string literal, a comment or a template expression. The output will be syntactically wrong and the source map will faithfully describe the wrong result. Tools built on top of magic-string typically pair it with a parser for exactly this reason, and the README does not claim otherwise.

The hires option is the second pressure point. The default, false, produces one mapping per line, and the README warns that devtools may then identify only the correct line, not the exact column. Turning on true fixes that at the cost of a mapping per character, which the README describes as bulkier. "experimental-range" is explicitly labelled experimental and depends on the consumer supporting range mappings, so it is not a safe default for a pipeline you do not control. There is also a practical limit on output size: every edit is retained in memory alongside the original string, which is the trade-off that makes the original-index rule possible.

Finally, the package is ESM-only. Projects still shipping CommonJS builds through require cannot import it without a loader or a build step. The repository's own package.json confirms this: the exports map points "." at ./dist/index.mjs and types at ./dist/index.d.mts, with no CommonJS entry.

magic-string against recast and source-map-js

recast, named in the README as the alternative the author considered, parses JavaScript into an AST, lets you modify nodes, and reprints the tree while preserving the original formatting and comments. The reprint step is where recast generates its map. That approach handles structural changes: renaming a binding, moving a function, deleting a branch. It also constrains you, because the reprint is driven by the printer's rules and by what the parser understood. magic-string inverts both properties. It handles arbitrary text, including languages that are not JavaScript, and it makes no attempt to keep the output parseable. The difference in approach is parse-and-reprint versus record-and-splice.

source-map-js appears in magic-string's devDependencies, which places it on the consuming side rather than as a rival: it is a map consumer used in tests, not a string editor. The runtime dependency list is short, just @jridgewell/sourcemap-codec for encoding and decoding mappings. That is a deliberate size choice, and it is why magic-string shows up inside larger tools rather than as a standalone CLI. The repository contains a benchmark directory and a bench script, so the author tracks performance, but the README publishes no numbers and none should be assumed.

Maintenance, licence and upgrade cost

The repository is not archived and the last push was on 2026-09-23, the same day as the v1.4.2 release. v1.4.0 and v1.4.1 both landed on 2026-09-15, so the recent cadence is a burst of patch and minor releases rather than a long quiet period. The licence is MIT, declared in package.json and in the LICENSE file at the repository root, which permits commercial use and modification provided the copyright notice and permission notice are retained. That is a statement of what the licence text says, not legal advice; if you are redistributing the package inside a product, have your own counsel read LICENSE.md.

Upgrade cost is shaped by the public surface. The README documents a stable set of methods and one options object, and the package is published as ESM with a types entry, so TypeScript consumers get declarations from dist/index.d.mts. The main compatibility risk is the sourcemap format rather than the API: if you rely on "experimental-range", a consumer that does not implement range mappings will not decode your map correctly. The repository ships a CHANGELOG.md generated by conventional-changelog, so release notes are the place to check before bumping a major.

Editorial conclusion

Adopt magic-string if you are writing a bundler, a transpiler or a code-wrapping step and your edits are positional rather than syntactic. Do not adopt it if you need to understand JavaScript structure, rename bindings or reprint an AST: it never parses your input, so it cannot tell you whether an index falls inside a string literal or a comment. Before committing, verify three things in the repository: that your toolchain can consume an ESM-only package, that the source map consumer in your pipeline supports the hires mode you plan to use, and that your edit sequence survives the documented rule that indices always refer to the original string.

Frequently asked questions

What is magic-string in JavaScript?

It is a utility, published as the npm package magic-string, for making light positional edits to a string and generating a version 3 source map from those edits. The README describes it as a small, fast utility for manipulating strings and generating sourcemaps.

How do I install magic-string?

For Node, the README gives npm i magic-string. In a browser, it is imported from an ESM CDN as https://unpkg.com/magic-string/dist/index.mjs, because the package is ESM-only.

How do I use magic-string to edit a string?

Construct a MagicString from the original text, call update with start and end indices plus the replacement, and read the result with toString. The README notes that character indices always refer to the original string, so later calls keep using original positions.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. Rich-Harris/magic-string 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/rich-harris-magic-string.svg)](https://hysenlabs.com/projects/rich-harris-magic-string)