How SWC Handles Source Maps and Debugging Support: A Deep Dive into the Architecture

SWC implements source map generation and debugging support through a centralized SourceMap architecture in swc_common that converts byte positions to line/column mappings, emits standard V3 source maps via swc_sourcemap, and exposes a proxy interface for WebAssembly plugins.

The swc-project/swc compiler relies on a sophisticated source mapping system to maintain the connection between transformed JavaScript output and original source code. This architecture powers accurate error reporting, debugging support, and plugin interoperability while preserving the performance characteristics that distinguish SWC from other transpilers. At its core, the system maps byte positions to line and column coordinates using a global SourceMap container that integrates seamlessly with the compiler's AST spans.

Core Architecture Components

SourceMap and SourceFile

The SourceMap struct in crates/swc_common/src/source_map.rs (lines 131-138) serves as the global container for all source files processed during a compilation session. It manages the mapping between raw byte positions and human-readable line/column coordinates.

Each input file is represented as a SourceFile, created via SourceMap::new_source_file (lines 219-227). This struct stores the file's raw bytes, its starting position in the global byte map, and pre-computed UTF-8/UTF-16 analysis data for accurate column calculations.

The map provides essential lookup methods like lookup_char_pos (lines 92-100) and span_to_snippet (lines 448-456), which convert Span values into actionable location data.

SourceMapBuilder and V3 Format Output

To emit standard source maps, SWC uses SourceMapBuilder from the swc_sourcemap crate, located in crates/swc_sourcemap/src/builder.rs. This component converts internal (BytePos, LineCol) pairs into the official V3 sourcemap JSON format.

The builder respects configuration options through the SourceMapGenConfig trait (implemented in crates/swc_ecma_transforms_testing/src/lib.rs around line 1120), controlling whether to inline source content, transform file names, or omit column data.

PluginSourceMapProxy for WebAssembly Plugins

When transforms run inside WebAssembly plugins, the host supplies a PluginSourceMapProxy defined in crates/swc_plugin_proxy/src/source_map/plugin_source_map_proxy.rs (lines 6-30). This proxy forwards lookup_char_pos and span_to_filename calls to the host's SourceMap, making source data available to plugins without requiring the full swc_common crate in the plugin binary.

The Source Map Generation Flow

  1. Registration: During parsing, the compiler calls SourceMap::new_source_file to register each SourceFile and reserve a contiguous range of global byte positions.

  2. Span tracking: AST nodes carry Span values represented as (BytePos, BytePos) tuples. When line/column information is needed, the compiler queries source_map.lookup_char_pos(span.lo()).

  3. Map construction: After transformation, the compiler collects mappings via source_map.build_source_map (lines 1134-1195), which delegates to swc_sourcemap::SourceMapBuilder to produce a standard swc_sourcemap::SourceMap object.

  4. Plugin access: Plugin code accesses the map through the generated proxy, which forwards calls to the host's implementation.

Working with the SourceMap API

Creating a SourceMap and Adding Files

use swc_common::{FileName, SourceMap, sync::Lrc};

let cm: Lrc<SourceMap> = Default::default();
let src = "const x = 10;".into();
let fm = cm.new_source_file(Lrc::new(FileName::Real("example.js".into())), src);

The new_source_file implementation resides in source_map.rs (lines 219-227), handling the UTF-8/UTF-16 analysis required for accurate column tracking.

Converting Spans to Debug Locations

use swc_common::Span;

let span = fm.span;                     // whole file span
let loc = cm.lookup_char_pos(span.lo()); // ← `Loc { file, line, col, … }`
println!("{}:{}:{}", loc.file.name, loc.line, loc.col.to_usize() + 1);

The lookup_char_pos method (lines 91-100) returns a Loc struct containing the file name, line number, and column offset.

Emitting Source Maps After Transformation

use swc_common::{SourceMap, BytePos, LineCol};
use swc_sourcemap::SourceMap as SwcSourceMap;

// Assume `cm` is the SourceMap used while parsing.
let mappings: Vec<(BytePos, LineCol)> = vec![
    // (generated byte position, original line/col)
    (BytePos(0), LineCol { line: 1, col: 0 }),
    // …
];
let config = swc_ecma_transforms_testing::SourceMapGenConfig::default();
let sourcemap: SwcSourceMap = cm.build_source_map(&mappings, None, config);
println!("{}", sourcemap.to_json_string());

The build_source_map method (lines 1134-1195) aggregates position data and delegates to SourceMapBuilder to generate the final JSON.

Accessing Source Maps in Plugins

// Inside a plugin (generated by the macro crate)
use swc_core::plugin::proxies::PluginSourceMapProxy;

// The host supplies `source_map: PluginSourceMapProxy` in the plugin metadata.
// Example: retrieve the original filename for a span.
let filename = plugin_meta.source_map.span_to_filename(span);

The proxy in plugin_source_map_proxy.rs (lines 6-30) forwards requests to the host's SourceMap instance.

Debugging Support and Diagnostic Integration

The SourceMap implements the SourceMapper trait (lines 3132-3168 in source_map.rs), allowing the diagnostics subsystem to convert BytePos values into human-readable locations without depending on the concrete map implementation.

For accurate error reporting in multi-byte UTF-8 contexts, the map uses calc_utf16_offset (lines 1195-1245) to compute column numbers that align with JavaScript debuggers and IDEs. Special handling for doctest offsets is available via SourceMap::doctest_offset_line (lines 80-88).

Summary

  • SWC uses a global SourceMap in swc_common to manage all source file metadata and byte-to-line mappings during compilation.
  • AST spans use BytePos tuples that resolve to line/column coordinates via lookup_char_pos and related methods.
  • Standard V3 source maps are generated through SourceMapBuilder in the swc_sourcemap crate, supporting configurable output via SourceMapGenConfig.
  • WebAssembly plugins access source data through PluginSourceMapProxy, enabling debugging support without bloating plugin binaries.
  • Diagnostic accuracy relies on UTF-8/UTF-16 analysis and the SourceMapper trait implementation for consistent error reporting.

Frequently Asked Questions

How does SWC maintain accurate column numbers for multi-byte characters?

SWC computes UTF-16 offsets using the calc_utf16_offset helper (lines 1195-1245 in source_map.rs) to ensure column numbers match JavaScript debugger expectations. Each SourceFile stores pre-computed UTF-8/UTF-16 analysis data when created via new_source_file, enabling accurate column calculations even for multibyte characters.

Can plugins generate their own source maps, or must they use the host's map?

Plugins must access source map data through the PluginSourceMapProxy provided by the host. This proxy forwards calls like lookup_char_pos and span_to_filename to the host's SourceMap, ensuring consistency while keeping plugin binaries lightweight and avoiding the need to link the full swc_common crate.

What source map format does SWC output?

SWC generates standard V3 source maps as defined by the Mozilla Source Map specification. The SourceMapBuilder in crates/swc_sourcemap/src/builder.rs handles the conversion from internal LineCol representations to the official JSON format, respecting options such as inline_sources_content and column omission.

How does SWC support incremental compilation with source maps?

Each SourceFile receives a stable identifier (StableSourceFileId) when registered with the SourceMap. This allows the compiler to reuse existing source mappings and byte position allocations across incremental builds, maintaining consistent span references without regenerating the entire map.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →