From f25e44df2456504ecad9ec87b1fce0eb4c9c548d Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 3 Dec 2025 14:54:41 +0100 Subject: [PATCH 1/4] wip --- src/encoder.rs | 9 ++++----- src/jsontypes.rs | 5 ++++- tests/test_encoder.rs | 35 +++++++++++++++++++++++++++++++++++ tests/test_index.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/encoder.rs b/src/encoder.rs index abc3286f..a473b2f0 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -213,9 +213,8 @@ 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(), + _debug_id_new: None, } } } @@ -308,8 +307,8 @@ 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")), + _debug_id_new: None, } ); } diff --git a/src/jsontypes.rs b/src/jsontypes.rs index 281ba6b4..19d4d0f7 100644 --- a/src/jsontypes.rs +++ b/src/jsontypes.rs @@ -54,7 +54,10 @@ pub struct RawSourceMap { pub x_metro_module_paths: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub x_facebook_sources: FacebookSources, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "debugId", deserialize = "debug_id"), + skip_serializing_if = "Option::is_none" + )] pub debug_id: Option, // This field only exists to be able to deserialize from "debugId" keys // if "debug_id" is unset. diff --git a/tests/test_encoder.rs b/tests/test_encoder.rs index b67f087d..a555de34 100644 --- a/tests/test_encoder.rs +++ b/tests/test_encoder.rs @@ -62,3 +62,38 @@ 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 = 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 + ); +} diff --git a/tests/test_index.rs b/tests/test_index.rs index b3983576..0540bb0e 100644 --- a/tests/test_index.rs +++ b/tests/test_index.rs @@ -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 + ); +} From db3e12e7f6602b9b86420a4898561a38d9573861 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 3 Dec 2025 15:34:41 +0100 Subject: [PATCH 2/4] custom deserialize --- src/decoder.rs | 45 ++++----------------------------- src/encoder.rs | 13 ++++------ src/jsontypes.rs | 59 +++++++++++++++++++++++++++++++++++-------- tests/test_encoder.rs | 5 +--- 4 files changed, 60 insertions(+), 62 deletions(-) diff --git a/src/decoder.rs b/src/decoder.rs index 1581d00f..371c53b1 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -267,9 +267,7 @@ pub fn decode_regular(rsm: RawSourceMap) -> Result { 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); @@ -307,7 +305,7 @@ fn decode_index(rsm: RawSourceMap) -> Result { 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 { @@ -356,6 +354,7 @@ pub fn decode_data_url(url: &str) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::jsontypes::DebugIdField; use std::io::{self, BufRead}; #[test] @@ -419,8 +418,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"); @@ -448,40 +446,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"); diff --git a/src/encoder.rs b/src/encoder.rs index a473b2f0..85f051d4 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -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(), } } } @@ -213,8 +212,7 @@ impl Encodable for SourceMapIndex { x_facebook_offsets: None, x_metro_module_paths: None, x_facebook_sources: None, - debug_id: self.debug_id(), - _debug_id_new: None, + debug_id: self.debug_id().into(), } } } @@ -232,6 +230,7 @@ impl Encodable for DecodedMap { #[cfg(test)] mod tests { use super::*; + use crate::jsontypes::DebugIdField; #[test] fn test_encode_rmi() { @@ -277,8 +276,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(), } ); } @@ -307,8 +305,7 @@ mod tests { 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(), } ); } diff --git a/src/jsontypes.rs b/src/jsontypes.rs index 19d4d0f7..7ab9aac5 100644 --- a/src/jsontypes.rs +++ b/src/jsontypes.rs @@ -1,7 +1,8 @@ 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 { @@ -54,15 +55,8 @@ pub struct RawSourceMap { pub x_metro_module_paths: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub x_facebook_sources: FacebookSources, - #[serde( - rename(serialize = "debugId", deserialize = "debug_id"), - skip_serializing_if = "Option::is_none" - )] - pub debug_id: Option, - // 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, + #[serde(flatten)] + pub debug_id: DebugIdField, } #[derive(Deserialize)] @@ -78,3 +72,48 @@ pub struct MinimalRawSourceMap { pub names: Option, pub mappings: Option, } + +/// 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 +/// logic, which can read both the legacy snake_case debug_id and the new camelCase debugId +/// 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, +} + +impl<'de> Deserialize<'de> for DebugIdField { + fn deserialize(deserializer: D) -> Result + 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, + #[serde(rename = "debug_id")] + legacy: Option, + } + + let Helper { camel, legacy } = Helper::deserialize(deserializer)?; + Ok(camel.or(legacy).into()) + } +} + +impl From> for DebugIdField { + fn from(value: Option) -> Self { + Self { value } + } +} + +impl From for Option { + fn from(value: DebugIdField) -> Self { + value.value + } +} diff --git a/tests/test_encoder.rs b/tests/test_encoder.rs index a555de34..840ddb87 100644 --- a/tests/test_encoder.rs +++ b/tests/test_encoder.rs @@ -78,10 +78,7 @@ fn test_sourcemap_serializes_camel_case_debug_id() { ); let sm = SourceMap::from_reader(input.as_bytes()).unwrap(); - let expected = sm - .get_debug_id() - .expect("debug id parsed") - .to_string(); + let expected = sm.get_debug_id().expect("debug id parsed").to_string(); let mut out: Vec = vec![]; sm.to_writer(&mut out).unwrap(); let serialized = String::from_utf8(out).unwrap(); From f7b60c0d1cf91537f018dca00b9f0932dbdace24 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 3 Dec 2025 16:34:54 +0100 Subject: [PATCH 3/4] add tests --- src/decoder.rs | 1 - src/encoder.rs | 1 - src/jsontypes.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/decoder.rs b/src/decoder.rs index 371c53b1..132f657d 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -354,7 +354,6 @@ pub fn decode_data_url(url: &str) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::jsontypes::DebugIdField; use std::io::{self, BufRead}; #[test] diff --git a/src/encoder.rs b/src/encoder.rs index 85f051d4..c4e5c8cf 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -230,7 +230,6 @@ impl Encodable for DecodedMap { #[cfg(test)] mod tests { use super::*; - use crate::jsontypes::DebugIdField; #[test] fn test_encode_rmi() { diff --git a/src/jsontypes.rs b/src/jsontypes.rs index 7ab9aac5..9185bf5a 100644 --- a/src/jsontypes.rs +++ b/src/jsontypes.rs @@ -117,3 +117,46 @@ impl From for Option { 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::(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 = raw.debug_id.into(); + assert_eq!(value, Some(parse_debug_id(camel))); + } +} From 36a3713f73145a1b5fc151923f15412beca9cc92 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 3 Dec 2025 16:45:40 +0100 Subject: [PATCH 4/4] fix test --- src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.rs b/src/types.rs index cc6fd7b2..908f9f75 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1441,8 +1441,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();