Skip to content

Commit fa1d488

Browse files
feat: add debugId support across all crates (ECMA-426)
Parses both `debugId` and `debug_id` fields from source maps, stores and roundtrips through to_json(). Added to parser, generator, CLI info command, NAPI bindings, and WASM bindings.
1 parent c89dde4 commit fa1d488

9 files changed

Lines changed: 154 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Node.js WASM bindings completing the full source map pipeline.
155155

156156
## Future
157157

158-
- [ ] Debug ID support (`debugId` field, part of ECMA-426)
158+
- [x] Debug ID support (`debugId` field, part of ECMA-426)
159159
- [ ] NAPI bindings for generator and remapping
160160
- [ ] WASM build target for browser (devtools, playgrounds, edge runtimes)
161161
- [ ] Scopes & variables support (ECMA-426 proposal — no library supports this yet)

crates/cli/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ fn cmd_info(file: &PathBuf, json: bool) -> Result<(), CliError> {
377377
"totalContentSize": content_size,
378378
"fileSize": raw.len(),
379379
"ignoreList": sm.ignore_list,
380+
"debugId": sm.debug_id,
380381
});
381382
println!("{}", serde_json::to_string_pretty(&obj).unwrap());
382383
} else {
@@ -407,6 +408,10 @@ fn cmd_info(file: &PathBuf, json: bool) -> Result<(), CliError> {
407408
);
408409
}
409410

411+
if let Some(ref id) = sm.debug_id {
412+
println!("Debug ID: {id}");
413+
}
414+
410415
if !sm.ignore_list.is_empty() {
411416
println!("Ignore list: {} sources", sm.ignore_list.len());
412417
}

crates/generator/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub struct SourceMapGenerator {
5151
names: Vec<String>,
5252
mappings: Vec<Mapping>,
5353
ignore_list: Vec<u32>,
54+
debug_id: Option<String>,
5455

5556
// Dedup maps for O(1) lookup
5657
source_map: HashMap<String, u32>,
@@ -68,6 +69,7 @@ impl SourceMapGenerator {
6869
names: Vec::new(),
6970
mappings: Vec::new(),
7071
ignore_list: Vec::new(),
72+
debug_id: None,
7173
source_map: HashMap::new(),
7274
name_map: HashMap::new(),
7375
}
@@ -78,6 +80,11 @@ impl SourceMapGenerator {
7880
self.source_root = Some(root);
7981
}
8082

83+
/// Set the debug ID (UUID) for this source map (ECMA-426).
84+
pub fn set_debug_id(&mut self, id: String) {
85+
self.debug_id = Some(id);
86+
}
87+
8188
/// Register a source file and return its index.
8289
pub fn add_source(&mut self, source: &str) -> u32 {
8390
if let Some(&idx) = self.source_map.get(source) {
@@ -441,6 +448,12 @@ impl SourceMapGenerator {
441448
json.push(']');
442449
}
443450

451+
// debugId
452+
if let Some(ref id) = self.debug_id {
453+
json.push_str(r#","debugId":"#);
454+
json.push_str(&json_quote(id));
455+
}
456+
444457
json.push('}');
445458
json
446459
}
@@ -767,6 +780,23 @@ mod tests {
767780
assert!(sm.original_position_for(5, 0).is_some());
768781
}
769782

783+
#[test]
784+
fn debug_id() {
785+
let mut builder = SourceMapGenerator::new(None);
786+
builder.set_debug_id("85314830-023f-4cf1-a267-535f4e37bb17".to_string());
787+
let src = builder.add_source("input.js");
788+
builder.add_mapping(0, 0, src, 0, 0);
789+
790+
let json = builder.to_json();
791+
assert!(json.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
792+
793+
let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
794+
assert_eq!(
795+
sm.debug_id.as_deref(),
796+
Some("85314830-023f-4cf1-a267-535f4e37bb17")
797+
);
798+
}
799+
770800
#[cfg(feature = "parallel")]
771801
mod parallel_tests {
772802
use super::*;

crates/sourcemap/src/lib.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ struct RawSourceMap<'a> {
117117
mappings: &'a str,
118118
#[serde(default, rename = "ignoreList")]
119119
ignore_list: Vec<u32>,
120+
/// Debug ID for associating generated files with source maps (ECMA-426).
121+
/// Accepts both `debugId` (spec) and `debug_id` (Sentry compat).
122+
#[serde(default, rename = "debugId", alias = "debug_id")]
123+
debug_id: Option<String>,
120124
/// Indexed source maps use `sections` instead of `mappings`.
121125
#[serde(default)]
122126
sections: Option<Vec<RawSection>>,
@@ -146,6 +150,8 @@ pub struct SourceMap {
146150
pub sources_content: Vec<Option<String>>,
147151
pub names: Vec<String>,
148152
pub ignore_list: Vec<u32>,
153+
/// Debug ID (UUID) for associating generated files with source maps (ECMA-426).
154+
pub debug_id: Option<String>,
149155

150156
/// Flat decoded mappings, ordered by (generated_line, generated_column).
151157
mappings: Vec<Mapping>,
@@ -213,6 +219,7 @@ impl SourceMap {
213219
sources_content,
214220
names: raw.names,
215221
ignore_list: raw.ignore_list,
222+
debug_id: raw.debug_id,
216223
mappings,
217224
line_offsets,
218225
reverse_index: OnceCell::new(),
@@ -354,6 +361,7 @@ impl SourceMap {
354361
sources_content: all_sources_content,
355362
names: all_names,
356363
ignore_list: all_ignore_list,
364+
debug_id: None,
357365
mappings: all_mappings,
358366
line_offsets,
359367
reverse_index: OnceCell::new(),
@@ -553,6 +561,11 @@ impl SourceMap {
553561
json.push(']');
554562
}
555563

564+
if let Some(ref id) = self.debug_id {
565+
json.push_str(r#","debugId":"#);
566+
json_quote_into(&mut json, id);
567+
}
568+
556569
json.push('}');
557570
json
558571
}
@@ -2069,6 +2082,51 @@ mod tests {
20692082
assert_eq!(sm2.source(loc.source), "a.js");
20702083
}
20712084

2085+
#[test]
2086+
fn parse_debug_id() {
2087+
let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
2088+
let sm = SourceMap::from_json(json).unwrap();
2089+
assert_eq!(
2090+
sm.debug_id.as_deref(),
2091+
Some("85314830-023f-4cf1-a267-535f4e37bb17")
2092+
);
2093+
}
2094+
2095+
#[test]
2096+
fn parse_debug_id_snake_case() {
2097+
let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debug_id":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
2098+
let sm = SourceMap::from_json(json).unwrap();
2099+
assert_eq!(
2100+
sm.debug_id.as_deref(),
2101+
Some("85314830-023f-4cf1-a267-535f4e37bb17")
2102+
);
2103+
}
2104+
2105+
#[test]
2106+
fn parse_no_debug_id() {
2107+
let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2108+
let sm = SourceMap::from_json(json).unwrap();
2109+
assert_eq!(sm.debug_id, None);
2110+
}
2111+
2112+
#[test]
2113+
fn debug_id_roundtrip() {
2114+
let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
2115+
let sm = SourceMap::from_json(json).unwrap();
2116+
let output = sm.to_json();
2117+
assert!(output.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
2118+
let sm2 = SourceMap::from_json(&output).unwrap();
2119+
assert_eq!(sm.debug_id, sm2.debug_id);
2120+
}
2121+
2122+
#[test]
2123+
fn debug_id_not_in_json_when_absent() {
2124+
let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2125+
let sm = SourceMap::from_json(json).unwrap();
2126+
let output = sm.to_json();
2127+
assert!(!output.contains("debugId"));
2128+
}
2129+
20722130
/// Generate a test source map JSON with realistic structure.
20732131
fn generate_test_sourcemap(lines: usize, segs_per_line: usize, num_sources: usize) -> String {
20742132
let sources: Vec<String> = (0..num_sources)

packages/generator-wasm/__tests__/generator-wasm.test.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ describe('ignoreList', () => {
149149
})
150150
})
151151

152+
describe('debugId', () => {
153+
it('sets and outputs debugId', () => {
154+
const gen = new SourceMapGenerator()
155+
gen.setDebugId('85314830-023f-4cf1-a267-535f4e37bb17')
156+
gen.addSource('input.js')
157+
gen.addGeneratedMapping(0, 0)
158+
159+
const map = JSON.parse(gen.toJSON())
160+
assert.equal(map.debugId, '85314830-023f-4cf1-a267-535f4e37bb17')
161+
gen.free()
162+
})
163+
164+
it('omits debugId when not set', () => {
165+
const gen = new SourceMapGenerator()
166+
gen.addGeneratedMapping(0, 0)
167+
168+
const map = JSON.parse(gen.toJSON())
169+
assert.equal(map.debugId, undefined)
170+
gen.free()
171+
})
172+
})
173+
152174
describe('large roundtrip', () => {
153175
it('handles 1000 mappings correctly', () => {
154176
const gen = new SourceMapGenerator('bundle.js')

packages/generator-wasm/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ impl SourceMapGenerator {
3333
self.inner.set_source_root(root.to_string());
3434
}
3535

36+
/// Set the debug ID (UUID) for this source map (ECMA-426).
37+
#[wasm_bindgen(js_name = "setDebugId")]
38+
pub fn set_debug_id(&mut self, id: &str) {
39+
self.inner.set_debug_id(id.to_string());
40+
}
41+
3642
/// Set the content for a source file by index.
3743
#[wasm_bindgen(js_name = "setSourceContent")]
3844
pub fn set_source_content(&mut self, source_idx: u32, content: &str) {

packages/sourcemap-wasm/__tests__/sourcemap-wasm.test.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,27 @@ describe('originalPositionsFor (batch)', () => {
122122
})
123123
})
124124

125+
describe('debugId', () => {
126+
it('returns debugId when present', () => {
127+
const map = JSON.stringify({
128+
version: 3,
129+
sources: ['a.js'],
130+
names: [],
131+
mappings: 'AAAA',
132+
debugId: '85314830-023f-4cf1-a267-535f4e37bb17',
133+
})
134+
const sm = new SourceMap(map)
135+
assert.equal(sm.debugId, '85314830-023f-4cf1-a267-535f4e37bb17')
136+
sm.free()
137+
})
138+
139+
it('returns undefined when debugId is absent', () => {
140+
const sm = new SourceMap(SIMPLE_MAP)
141+
assert.equal(sm.debugId, undefined)
142+
sm.free()
143+
})
144+
})
145+
125146
describe('indexed source maps', () => {
126147
it('parses an indexed (sectioned) source map', () => {
127148
const indexedMap = JSON.stringify({

packages/sourcemap-wasm/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ impl SourceMap {
8585
.collect()
8686
}
8787

88+
/// Get the debug ID (UUID) if present.
89+
#[wasm_bindgen(getter, js_name = "debugId")]
90+
pub fn debug_id(&self) -> Option<String> {
91+
self.inner.debug_id.clone()
92+
}
93+
8894
/// Total number of decoded mappings.
8995
#[wasm_bindgen(getter, js_name = "mappingCount")]
9096
pub fn mapping_count(&self) -> u32 {

packages/sourcemap/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ impl JsSourceMap {
101101
results
102102
}
103103

104+
#[napi(getter)]
105+
pub fn debug_id(&self) -> Option<String> {
106+
self.inner.debug_id.clone()
107+
}
108+
104109
#[napi(getter)]
105110
pub fn mapping_count(&self) -> u32 {
106111
self.inner.mapping_count() as u32

0 commit comments

Comments
 (0)