Skip to content

Commit

Permalink
Add JSON and data URL parsing and serialization support to Rust API (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Jul 4, 2022
1 parent d84c22d commit c28baca
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
run: yarn run build:node-release
- name: Run node tests
run: yarn run test:node
- name: Run rust tests
run: cargo test --all-features

test-wasm:
name: wasm
Expand Down
29 changes: 27 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions parcel_sourcemap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ repository = "https://github.com/parcel-bundler/source-map"
[dependencies]
"vlq" = "0.5.1"
rkyv = "0.7.38"
serde = {version = "1", features = ["derive"], optional = true}
serde_json = { version = "1", optional = true }
base64 = { version = "0.13.0", optional = true }
data-url = { version = "0.1.1", optional = true }

[features]
json = ["serde", "serde_json", "base64", "data-url"]
158 changes: 148 additions & 10 deletions parcel_sourcemap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ pub mod utils;
mod vlq_utils;

use crate::utils::make_relative_path;
#[cfg(feature = "json")]
use data_url::DataUrl;
pub use mapping::{Mapping, OriginalLocation};
use mapping_line::MappingLine;
pub use sourcemap_error::{SourceMapError, SourceMapErrorType};
#[cfg(feature = "json")]
use std::borrow::Cow;
use std::io;

use rkyv::{
Expand Down Expand Up @@ -229,11 +233,11 @@ impl SourceMap {
}
}

pub fn add_sources(&mut self, sources: Vec<&str>) -> Vec<u32> {
pub fn add_sources<I: AsRef<str>>(&mut self, sources: Vec<I>) -> Vec<u32> {
self.inner.sources.reserve(sources.len());
let mut result_vec = Vec::with_capacity(sources.len());
for s in sources.iter() {
result_vec.push(self.add_source(s));
result_vec.push(self.add_source(s.as_ref()));
}
result_vec
}
Expand Down Expand Up @@ -270,9 +274,9 @@ impl SourceMap {
};
}

pub fn add_names(&mut self, names: Vec<&str>) -> Vec<u32> {
pub fn add_names<I: AsRef<str>>(&mut self, names: Vec<I>) -> Vec<u32> {
self.inner.names.reserve(names.len());
return names.iter().map(|n| self.add_name(n)).collect();
return names.iter().map(|n| self.add_name(n.as_ref())).collect();
}

pub fn get_name_index(&self, name: &str) -> Option<u32> {
Expand Down Expand Up @@ -510,12 +514,12 @@ impl SourceMap {
Ok(())
}

pub fn add_vlq_map(
pub fn add_vlq_map<I: AsRef<str>>(
&mut self,
input: &[u8],
sources: Vec<&str>,
sources_content: Vec<&str>,
names: Vec<&str>,
sources: Vec<I>,
sources_content: Vec<I>,
names: Vec<I>,
line_offset: i64,
column_offset: i64,
) -> Result<(), SourceMapError> {
Expand All @@ -532,7 +536,7 @@ impl SourceMap {
self.inner.sources_content.reserve(sources_content.len());
for (i, source_content) in sources_content.iter().enumerate() {
if let Some(source_index) = source_indexes.get(i) {
self.set_source_content(*source_index as usize, source_content)?;
self.set_source_content(*source_index as usize, source_content.as_ref())?;
}
}

Expand Down Expand Up @@ -627,7 +631,7 @@ impl SourceMap {
}

let line = generated_line as usize;
let abs_offset = generated_line_offset.abs() as usize;
let abs_offset = generated_line_offset.unsigned_abs() as usize;
if generated_line_offset > 0 {
if line > self.inner.mapping_lines.len() {
self.ensure_lines(line + abs_offset);
Expand Down Expand Up @@ -670,6 +674,85 @@ impl SourceMap {

Ok(())
}

#[cfg(feature = "json")]
pub fn from_json<'a>(project_root: &str, input: &'a str) -> Result<Self, SourceMapError> {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct JSONSourceMap<'a> {
mappings: &'a str,
#[serde(borrow)]
sources: Vec<Cow<'a, str>>,
sources_content: Vec<Cow<'a, str>>,
names: Vec<Cow<'a, str>>,
}

let json: JSONSourceMap = serde_json::from_str(input)?;
let mut sm = Self::new(project_root);
sm.add_vlq_map(
json.mappings.as_bytes(),
json.sources,
json.sources_content,
json.names,
0,
0,
)?;
Ok(sm)
}

#[cfg(feature = "json")]
pub fn to_json(&mut self, source_root: Option<&str>) -> Result<String, SourceMapError> {
let mut vlq_output: Vec<u8> = Vec::new();
self.write_vlq(&mut vlq_output)?;

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct JSONSourceMap<'a> {
version: u8,
source_root: Option<&'a str>,
mappings: &'a str,
sources: &'a Vec<String>,
sources_content: &'a Vec<String>,
names: &'a Vec<String>,
}

let sm = JSONSourceMap {
version: 3,
source_root,
mappings: unsafe { std::str::from_utf8_unchecked(&vlq_output) },
sources: self.get_sources(),
sources_content: self.get_sources_content(),
names: self.get_names(),
};

Ok(serde_json::to_string(&sm)?)
}

#[cfg(feature = "json")]
pub fn from_data_url(project_root: &str, data_url: &str) -> Result<Self, SourceMapError> {
let url = DataUrl::process(&data_url)?;
let mime = url.mime_type();
if mime.type_ != "application" || mime.subtype != "json" {
return Err(SourceMapError::new(SourceMapErrorType::DataUrlError));
}

let (data, _) = url
.decode_to_vec()
.map_err(|_| SourceMapError::new(SourceMapErrorType::DataUrlError))?;
let input = unsafe { std::str::from_utf8_unchecked(data.as_slice()) };

Self::from_json(project_root, input)
}

#[cfg(feature = "json")]
pub fn to_data_url(&mut self, source_root: Option<&str>) -> Result<String, SourceMapError> {
let buf = self.to_json(source_root)?;
let b64 = base64::encode(&buf);
Ok(format!(
"data:application/json;charset=utf-8;base64,{}",
b64
))
}
}

#[allow(non_fmt_panics)]
Expand All @@ -688,3 +771,58 @@ fn test_buffers() {
Err(err) => panic!(err),
}
}

#[cfg(feature = "json")]
#[test]
fn test_to_json() {
let mut map = SourceMap::new("/");
map.add_mapping(1, 1, None);
let json = map.to_json(Some("/")).unwrap();
assert_eq!(
json,
r#"{"version":3,"sourceRoot":"/","mappings":";C","sources":[],"sourcesContent":[],"names":[]}"#
);
}

#[cfg(feature = "json")]
#[test]
fn test_from_json() {
let map = SourceMap::from_json("/", r#"{"version":3,"sourceRoot":"/","mappings":";C","sources":[],"sourcesContent":[],"names":[]}"#).unwrap();
let mappings = map.get_mappings();
assert_eq!(
mappings,
vec![Mapping {
generated_line: 1,
generated_column: 1,
original: None
}]
);
}

#[cfg(feature = "json")]
#[test]
fn test_to_data_url() {
let mut map = SourceMap::new("/");
map.add_mapping(1, 1, None);
let url = map.to_data_url(Some("/")).unwrap();
println!("{}", url);
assert_eq!(
url,
r#"data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290IjoiLyIsIm1hcHBpbmdzIjoiO0MiLCJzb3VyY2VzIjpbXSwic291cmNlc0NvbnRlbnQiOltdLCJuYW1lcyI6W119"#
);
}

#[cfg(feature = "json")]
#[test]
fn test_from_data_url() {
let map = SourceMap::from_data_url("/", r#"data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290IjoiLyIsIm1hcHBpbmdzIjoiO0MiLCJzb3VyY2VzIjpbXSwic291cmNlc0NvbnRlbnQiOltdLCJuYW1lcyI6W119"#).unwrap();
let mappings = map.get_mappings();
assert_eq!(
mappings,
vec![Mapping {
generated_line: 1,
generated_column: 1,
original: None
}]
);
}
4 changes: 2 additions & 2 deletions parcel_sourcemap/src/mapping.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use rkyv::{Archive, Deserialize, Serialize};

#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy)]
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct OriginalLocation {
pub original_line: u32,
pub original_column: u32,
Expand All @@ -19,7 +19,7 @@ impl OriginalLocation {
}
}

#[derive(Archive, Serialize, Deserialize, Debug)]
#[derive(Archive, Serialize, Deserialize, Debug, PartialEq)]
pub struct Mapping {
pub generated_line: u32,
pub generated_column: u32,
Expand Down
2 changes: 1 addition & 1 deletion parcel_sourcemap/src/mapping_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ impl MappingLine {
index = start_index;
}

let abs_offset = generated_column_offset.abs() as u32;
let abs_offset = generated_column_offset.unsigned_abs() as u32;
for i in index..self.mappings.len() {
let mapping = &mut self.mappings[i];
mapping.generated_column = if generated_column_offset < 0 {
Expand Down
30 changes: 30 additions & 0 deletions parcel_sourcemap/src/sourcemap_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ pub enum SourceMapErrorType {

// Failed to convert utf-8 to array
FromUtf8Error = 11,

// Failed to serialize to JSON
JSONError = 12,

// Failed to parse data url
#[cfg(feature = "json")]
DataUrlError = 13,
}

#[derive(Debug)]
Expand Down Expand Up @@ -107,6 +114,13 @@ impl std::fmt::Display for SourceMapError {
SourceMapErrorType::FromUtf8Error => {
write!(f, "Could not convert utf-8 array to string")?;
}
SourceMapErrorType::JSONError => {
write!(f, "Error reading or writing to JSON")?;
}
#[cfg(feature = "json")]
SourceMapErrorType::DataUrlError => {
write!(f, "Error parsing data url")?;
}
}

// Add reason to error string if there is one
Expand Down Expand Up @@ -162,3 +176,19 @@ impl From<std::string::FromUtf8Error> for SourceMapError {
SourceMapError::new(SourceMapErrorType::FromUtf8Error)
}
}

#[cfg(feature = "json")]
impl From<serde_json::Error> for SourceMapError {
#[inline]
fn from(_err: serde_json::Error) -> SourceMapError {
SourceMapError::new(SourceMapErrorType::JSONError)
}
}

#[cfg(feature = "json")]
impl From<data_url::DataUrlError> for SourceMapError {
#[inline]
fn from(_err: data_url::DataUrlError) -> SourceMapError {
SourceMapError::new(SourceMapErrorType::DataUrlError)
}
}

1 comment on commit c28baca

@github-actions
Copy link

Choose a reason for hiding this comment

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

Parcel sourcemap benchmark

Benchmark suite Current: c28baca Previous: d84c22d Ratio
consume#consume buffer 144133 ops/sec (±13%) 72015 ops/sec (±1.2e+2%) 0.50
consume#consume JS Mappings 47436 ops/sec (±6.4%) 59322 ops/sec (±5.8%) 1.25
consume#consume vlq mappings 36336 ops/sec (±17%) 55847 ops/sec (±15%) 1.54
serialize#Save buffer 312 ops/sec (±2.3%) 404 ops/sec (±0.76%) 1.29
serialize#Serialize to vlq 247 ops/sec (±3.1%) 290 ops/sec (±0.79%) 1.17
modify#negative column offset 58695 ops/sec (±11%) 155714 ops/sec (±24%) 2.65
modify#positive column offset 54156 ops/sec (±15%) 141262 ops/sec (±30%) 2.61
modify#positive line offset 31941 ops/sec (±6.7%) 48407 ops/sec (±7.8%) 1.52
modify#negative line offset 30287 ops/sec (±4.3%) 49034 ops/sec (±5.7%) 1.62
append#addSourceMap 165 ops/sec (±0.43%) 229 ops/sec (±1.5%) 1.39

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.