magic_string

Package VersionHex Docs

Edit a string by byte offset and get a source map out the other side. A Gleam port of Rich Harris's magic-string.

It's useful when you have some source code, you know where the bits you care about are (because a parser told you), and you want to make surgical changes without rebuilding the whole thing from an AST. The source map is a byproduct of the edit log, so you don't have to think about it.

gleam add magic_string
import magic_string as ms
import magic_string/codec
pub fn main() {
let s = ms.new("const answer = 42;")
// Byte offsets are half-open: [start, end). overwrite/remove return a
// Result because they can conflict with earlier edits (see below).
let assert Ok(s) = ms.overwrite(s, 6, 12, "x")
let assert Ok(s) = ms.remove(s, 0, 6)
let s = ms.append(s, " // edited")
ms.to_string(s)
// -> "x = 42; // edited"
let map = ms.generate_map(s, "in.js", ms.default_map_options())
codec.to_json(map)
// -> {"version":3,"sources":["in.js"],"mappings":"...",...}
}

Bundling multiple sources

import magic_string as ms
let a = "export const a = 1;"
let b = "export const b = 2;"
let bundle =
ms.bundle()
|> ms.add_source("a.js", a, ms.new(a))
|> ms.add_source("b.js", b, ms.new(b))
ms.bundle_to_string(bundle)
ms.bundle_generate_map(bundle, ms.default_map_options())

The map has sources and sourcesContent populated for every file you added, so a debugger can show the original code.

API

The full API lives in the hexdocs. The short version:

Notes

Offsets are UTF-8 byte offsets, not character indices. On the BEAM that's what string.byte_size and friends give you, and it's what most parsers report. The position index handles the conversion to UTF-16 columns for the source map output.

overwrite, remove, append_left and append_right return Result(MagicString, EditError). They fail when the edit conflicts with one already recorded: two ranges overlapping (Overlap), an insert falling inside an existing range or a new range swallowing an existing insert (SwallowedInsert), or offsets outside the source (OutOfBounds, InvertedRange). The error names the offsets involved, so you can trace it back to the bad span. Adjacent edits that share a boundary but no bytes are fine. prepend, append, to_string and generate_map are infallible, since by the time you reach them every recorded edit is already known to compose.

In a bundler the offsets come straight from a parser's spans, so in practice you result.try through a chain of edits and surface the first conflict as a build error.

Also see: my project Arc!