Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 9.3.0

### New Features ✨

- feat(debugId): Serialize source maps with `debugId`, not `debug_id` by @szokeasaurusrex in [#134](https://github.com/getsentry/rust-sourcemap/pull/134)

### Build / dependencies / internal 🔧

- chore: Fix 1.88.0 clippy lints by @loewenheim in [#130](https://github.com/getsentry/rust-sourcemap/pull/130)

### Other

- Store SourceView linecache as offsets rather than pointers by @coolreader18 in [#133](https://github.com/getsentry/rust-sourcemap/pull/133)

## 9.2.2

### Various fixes & improvements
Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cli"
version = "9.2.2"
version = "9.3.0"
authors = ["Armin Ronacher <[email protected]>"]
edition = "2018"

Expand Down
44 changes: 4 additions & 40 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,7 @@ pub fn decode_regular(rsm: RawSourceMap) -> Result<SourceMap> {

let mut sm = SourceMap::new(file, tokens, names, sources, source_content);
sm.set_source_root(rsm.source_root);
// Use _debug_id_new (from "debugId" key) only if debug_id
// from ( "debug_id" key) is unset
sm.set_debug_id(rsm.debug_id.or(rsm._debug_id_new));
sm.set_debug_id(rsm.debug_id.into());
if let Some(ignore_list) = rsm.ignore_list {
for idx in ignore_list {
sm.add_to_ignore_list(idx);
Expand Down Expand Up @@ -307,7 +305,7 @@ fn decode_index(rsm: RawSourceMap) -> Result<SourceMapIndex> {
rsm.x_facebook_offsets,
rsm.x_metro_module_paths,
)
.with_debug_id(rsm._debug_id_new.or(rsm.debug_id)))
.with_debug_id(rsm.debug_id.into()))
}

fn decode_common(rsm: RawSourceMap) -> Result<DecodedMap> {
Expand Down Expand Up @@ -419,8 +417,7 @@ mod tests {
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: None,
_debug_id_new: None,
debug_id: None.into(),
};

let decoded = decode_common(raw).expect("should decoded");
Expand Down Expand Up @@ -448,40 +445,7 @@ mod tests {
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: None,
_debug_id_new: Some(DEBUG_ID.parse().expect("valid debug id")),
};

let decoded = decode_common(raw).expect("should decode");
assert_eq!(
decoded,
DecodedMap::Index(
SourceMapIndex::new(Some("test.js".into()), vec![])
.with_debug_id(Some(DEBUG_ID.parse().expect("valid debug id")))
)
);
}

#[test]
fn test_decode_sourcemap_index_debug_id_from_legacy_key() {
const DEBUG_ID: &str = "0123456789abcdef0123456789abcdef";

let raw = RawSourceMap {
version: Some(3),
file: Some("test.js".into()),
sources: None,
source_root: None,
sources_content: None,
sections: Some(vec![]),
names: None,
range_mappings: None,
mappings: None,
ignore_list: None,
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: Some(DEBUG_ID.parse().expect("valid debug id")),
_debug_id_new: None,
debug_id: Some(DEBUG_ID.parse().expect("valid debug id")).into(),
};

let decoded = decode_common(raw).expect("should decode");
Expand Down
13 changes: 4 additions & 9 deletions src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ impl Encodable for SourceMap {
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: self.get_debug_id(),
_debug_id_new: None,
debug_id: self.get_debug_id().into(),
}
}
}
Expand Down Expand Up @@ -213,9 +212,7 @@ impl Encodable for SourceMapIndex {
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: None,
// Put the debug ID on _debug_id_new to serialize it to the debugId field.
_debug_id_new: self.debug_id(),
debug_id: self.debug_id().into(),
}
}
}
Expand Down Expand Up @@ -278,8 +275,7 @@ mod tests {
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: None,
_debug_id_new: None,
debug_id: None.into(),
}
);
}
Expand Down Expand Up @@ -308,8 +304,7 @@ mod tests {
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
debug_id: None,
_debug_id_new: Some(DEBUG_ID.parse().expect("valid debug id")),
debug_id: Some(DEBUG_ID.parse().expect("valid debug id")).into(),
}
);
}
Expand Down
99 changes: 92 additions & 7 deletions src/jsontypes.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use bytes_str::BytesStr;
use debugid::DebugId;
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::fmt::Debug;

#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct RawSectionOffset {
Expand Down Expand Up @@ -55,12 +56,8 @@ pub struct RawSourceMap {
pub x_metro_module_paths: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub x_facebook_sources: FacebookSources,
#[serde(skip_serializing_if = "Option::is_none")]
pub debug_id: Option<DebugId>,
// This field only exists to be able to deserialize from "debugId" keys
// if "debug_id" is unset.
#[serde(skip_serializing_if = "Option::is_none", rename = "debugId")]
pub(crate) _debug_id_new: Option<DebugId>,
#[serde(flatten)]
pub debug_id: DebugIdField,
}

#[derive(Deserialize)]
Expand All @@ -76,3 +73,91 @@ pub struct MinimalRawSourceMap {
pub names: Option<IgnoredAny>,
pub mappings: Option<IgnoredAny>,
}

/// This struct represents a `RawSourceMap`'s debug ID fields.
///
/// The reason this exists as a seperate struct is so that we can have custom deserialization
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: "seperate" should be "separate".

Suggested change
/// The reason this exists as a seperate struct is so that we can have custom deserialization
/// The reason this exists as a separate struct is so that we can have custom deserialization

Copilot uses AI. Check for mistakes.
/// logic, which can read both the legacy snake_case debug_id and the new camelCase debugId
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra whitespace: There are two spaces between "the" and "legacy" in the comment. Should be: "which can read both the legacy snake_case debug_id".

Suggested change
/// logic, which can read both the legacy snake_case debug_id and the new camelCase debugId
/// logic, which can read both the legacy snake_case debug_id and the new camelCase debugId

Copilot uses AI. Check for mistakes.
/// fields. In case both are provided, the camelCase field takes precedence.
///
/// The field is always serialized as `debugId`.
#[derive(Serialize, Clone, PartialEq, Debug, Default)]
pub(crate) struct DebugIdField {
#[serde(rename = "debugId", skip_serializing_if = "Option::is_none")]
value: Option<DebugId>,
}

impl<'de> Deserialize<'de> for DebugIdField {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// We cannot use serde(alias), as that would cause an error when both fields are present.

#[derive(Deserialize)]
struct Helper {
#[serde(rename = "debugId")]
camel: Option<DebugId>,
#[serde(rename = "debug_id")]
legacy: Option<DebugId>,
}

let Helper { camel, legacy } = Helper::deserialize(deserializer)?;
Ok(camel.or(legacy).into())
}
}

impl From<Option<DebugId>> for DebugIdField {
fn from(value: Option<DebugId>) -> Self {
Self { value }
}
}

impl From<DebugIdField> for Option<DebugId> {
fn from(value: DebugIdField) -> Self {
value.value
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

fn parse_debug_id(input: &str) -> DebugId {
input.parse().expect("valid debug id")
}

fn empty_sourcemap() -> RawSourceMap {
serde_json::from_value::<RawSourceMap>(serde_json::json!({}))
.expect("can deserialize empty JSON to RawSourceMap")
}

#[test]
fn raw_sourcemap_serializes_camel_case_debug_id() {
let camel = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
let raw = RawSourceMap {
debug_id: Some(parse_debug_id(camel)).into(),
..empty_sourcemap()
};

let value = serde_json::to_value(raw).expect("should serialize without error");
let obj = value.as_object().expect("should be an object");
assert!(obj.get("debug_id").is_none());
assert_eq!(obj.get("debugId"), Some(&json!(parse_debug_id(camel))));
}

#[test]
fn raw_sourcemap_prefers_camel_case_on_deserialize() {
let legacy = "ffffffffffffffffffffffffffffffff";
let camel = "00000000000000000000000000000000";
let json = serde_json::json!({
"debug_id": legacy,
"debugId": camel
});
let raw: RawSourceMap =
serde_json::from_value(json).expect("can deserialize as RawSourceMap");
let value: Option<DebugId> = raw.debug_id.into();
assert_eq!(value, Some(parse_debug_id(camel)));
}
}
4 changes: 2 additions & 2 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1665,8 +1665,8 @@ mod tests {
"sources":["coolstuff.js"],
"names":["x","alert"],
"mappings":"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
"debug_id":"00000000-0000-0000-0000-000000000000",
"debugId": "11111111-1111-1111-1111-111111111111"
"debug_id": "11111111-1111-1111-1111-111111111111",
"debugId":"00000000-0000-0000-0000-000000000000"
}"#;

let sm = SourceMap::from_slice(input).unwrap();
Expand Down
32 changes: 32 additions & 0 deletions tests/test_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,35 @@ fn test_empty_range() {
let out = String::from_utf8(out).unwrap();
assert!(!out.contains("rangeMappings"));
}

#[test]
fn test_sourcemap_serializes_camel_case_debug_id() {
const DEBUG_ID: &str = "0123456789abcdef0123456789abcdef";
let input = format!(
r#"{{
"version": 3,
"sources": [],
"names": [],
"mappings": "",
"debug_id": "{}"
}}"#,
DEBUG_ID
);

let sm = SourceMap::from_reader(input.as_bytes()).unwrap();
let expected = sm.get_debug_id().expect("debug id parsed").to_string();
let mut out: Vec<u8> = vec![];
sm.to_writer(&mut out).unwrap();
let serialized = String::from_utf8(out).unwrap();

assert!(
serialized.contains(&format!(r#""debugId":"{}""#, expected)),
"expected camelCase debugId in {}",
serialized
);
assert!(
!serialized.contains("debug_id"),
"unexpected snake_case key in {}",
serialized
);
}
30 changes: 30 additions & 0 deletions tests/test_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,33 @@ fn test_flatten_indexed_sourcemap_with_ignore_list() {
vec![1]
);
}

#[test]
fn test_sourcemap_index_serializes_camel_case_debug_id() {
const DEBUG_ID: &str = "fedcba9876543210fedcba9876543210";
let input = format!(
r#"{{
"version": 3,
"file": "bundle.js",
"sections": [],
"debugId": "{}"
}}"#,
DEBUG_ID
);

let smi = SourceMapIndex::from_reader(input.as_bytes()).unwrap();
let mut out = Vec::new();
smi.to_writer(&mut out).unwrap();
let serialized = String::from_utf8(out).unwrap();

assert!(
serialized.contains(r#""debugId":"#),
"expected camelCase debugId in {}",
serialized
);
assert!(
!serialized.contains("debug_id"),
"unexpected snake_case key in {}",
serialized
);
}