diff --git a/src/lib.rs b/src/lib.rs index d553146..02032ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,12 @@ pub fn sort_package_json_with_options( input: &str, options: &SortOptions, ) -> Result { - let value: Value = serde_json::from_str(input)?; + // Check for UTF-8 BOM and strip it for parsing + const BOM: char = '\u{FEFF}'; + let input_without_bom = input.strip_prefix(BOM).unwrap_or(input); + let has_bom = input_without_bom.len() != input.len(); + + let value: Value = serde_json::from_str(input_without_bom)?; let sorted_value = if let Value::Object(obj) = value { Value::Object(sort_object_keys(obj, options)) @@ -36,7 +41,15 @@ pub fn sort_package_json_with_options( serde_json::to_string(&sorted_value)? }; - Ok(result) + // Preserve BOM if it was present in the input + if has_bom { + let mut output = String::with_capacity(BOM.len_utf8() + result.len()); + output.push(BOM); + output.push_str(&result); + Ok(output) + } else { + Ok(result) + } } /// Sorts a package.json string with default options (pretty-printed) diff --git a/tests/fixtures/package-bom.json b/tests/fixtures/package-bom.json new file mode 100644 index 0000000..684486d --- /dev/null +++ b/tests/fixtures/package-bom.json @@ -0,0 +1,8 @@ +{ + "version": "1.0.0", + "exports": { + ".": "./index.mjs" + }, + "name": "@vitejs/test-utf8-bom-package", + "private": true +} \ No newline at end of file diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 0ea6093..7b549ec 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -68,3 +68,31 @@ fn test_size_limit_preservation() { let ignore_array = ignore.as_array().unwrap(); assert_eq!(ignore_array.len(), 3, "ignore should have 3 elements"); } + +#[test] +fn test_utf8_bom_preservation() { + // Test case based on https://github.com/vitejs/vite/blob/main/playground/resolve/utf8-bom-package/package.json + const BOM: char = '\u{FEFF}'; + + // Test 1: Files with BOM preserve it + let input = + fs::read_to_string("tests/fixtures/package-bom.json").expect("Failed to read BOM fixture"); + assert!(input.starts_with(BOM), "Fixture should have BOM"); + + let result = sort(&input); + assert!(result.starts_with(BOM), "BOM should be preserved in output"); + + let json_without_bom = &result[BOM.len_utf8()..]; + let parsed: Value = + serde_json::from_str(json_without_bom).expect("Output should be valid JSON after BOM"); + assert_eq!(parsed.get("name").and_then(|v| v.as_str()), Some("@vitejs/test-utf8-bom-package")); + + // Test 2: Files without BOM don't get BOM added + let input_no_bom = r#"{"version": "1.0.0", "name": "test"}"#; + let result_no_bom = sort(input_no_bom); + assert!(!result_no_bom.starts_with(BOM), "BOM should not be added if not present"); + + // Test 3: Idempotency - sorting twice produces same result + let second_sort = sort(&result); + assert_eq!(result, second_sort, "Sorting BOM files should be idempotent"); +}