/magic-string-sourcemap
When to Use
- Patch source code in a codemod/transform pipeline while keeping source maps valid
- Inject imports, decorators, or license headers at the top of a file
- Remove dead code blocks from a string without re-parsing the AST
- Bundle multiple source files into one string with accurate per-file source maps
Do NOT use for
- Heavy AST restructuring (use [[babel-ast-transform]] or [[ts-morph-refactor]])
- Non-source string manipulation (use regular string methods)
Basic mutations
import MagicString from 'magic-string'
const s = new MagicString(`export const VERSION = 'dev'`)
// Overwrite a range: positions are character offsets in the original string
s.overwrite(23, 28, '1.0.0') // 'dev' → '1.0.0'
// Insert before/after a position
s.prependLeft(0, '// Auto-generated\n')
s.appendRight(s.length(), '\n\nexport default VERSION')
// Remove a range (e.g., strip a debug block)
s.remove(30, 45)
console.log(s.toString())
// // Auto-generated
// export const VERSION = '1.0.0'
// export default VERSION
Generate source map
import MagicString from 'magic-string'
const s = new MagicString(sourceCode, {
filename: 'src/config.ts', // for source map 'sources' entry
indentExclusionRanges: [],
})
// ... mutations ...
const map = s.generateMap({
source: 'src/config.ts',
file: 'dist/config.js',
includeContent: true, // embed original source in map
hires: true, // character-level accuracy (not just line-level)
})
// Write output
writeFileSync('dist/config.js', s.toString())
writeFileSync('dist/config.js.map', map.toString())
Bundle multiple files with combined source map
import MagicString, { Bundle } from 'magic-string'
const bundle = new Bundle()
for (const { filename, source } of files) {
const s = new MagicString(source, { filename })
// optional per-file mutations
s.prepend(`// --- ${filename} ---\n`)
bundle.addSource({ filename, content: s })
}
const output = bundle.toString()
const map = bundle.generateMap({ includeContent: true, hires: true })
writeFileSync('bundle.js', output)
writeFileSync('bundle.js.map', map.toString())
Inject import at top of file
function injectImport(source: string, importStatement: string): string {
const s = new MagicString(source)
// Find first non-comment line to insert after
const firstNonComment = source.search(/^[^/\n]/)
const insertAt = firstNonComment > 0 ? firstNonComment : 0
s.prependLeft(insertAt, importStatement + '\n')
return s.toString()
}
const patched = injectImport(
readFileSync('src/app.ts', 'utf8'),
"import { logger } from './logger'"
)
Anti-Fake-Pass Checklist
❌ Overlapping overwrite ranges → MagicString throws; check ranges don't overlap before patching
❌ hires: false → line-level source maps only; column positions in debugger are wrong
❌ s.remove then s.overwrite same range → invalid state; use one operation per range
❌ Not writing .map file → source maps referenced in JS but 404 in browser devtools
❌ MagicString positions are UTF-16 code units — be careful with multi-byte Unicode
❌ bundle.addSource without filename → all sources merged under undefined in source map