use std::{cell::Cell, hash::Hash, sync::Arc};
use napi_derive::napi;
use rspack_core::rspack_sources::{
BoxSource, CachedSource, ConcatSource, MapOptions, ObjectPool, OriginalSource, RawBufferSource,
RawStringSource, ReplaceSource, Source, SourceExt, SourceMap, SourceMapSource, SourceValue,
WithoutOriginalOptions,
};
use rspack_napi::napi::bindgen_prelude::*;
use crate::error::RspackResultToNapiResultExt;
#[napi(object)]
pub struct JsSourceFromJs<'jsobject> {
pub source: Either<String, BufferSlice<'jsobject>>,
pub map: Option<String>,
}
impl<'jsobject> TryFrom<JsSourceFromJs<'jsobject>> for BoxSource {
type Error = napi::Error;
fn try_from(value: JsSourceFromJs<'jsobject>) -> Result<Self> {
match value.source {
Either::A(string) => {
if let Some(json) = value.map {
let source_map =
SourceMap::from_json(json).map_err(|e| napi::Error::from_reason(format!("{e}")))?;
Ok(
SourceMapSource::new(WithoutOriginalOptions {
value: string,
name: "inmemory://from js",
source_map,
})
.boxed(),
)
} else {
Ok(RawStringSource::from(string).boxed())
}
}
Either::B(buffer) => Ok(RawBufferSource::from(buffer.to_vec()).boxed()),
}
}
}
#[napi(object)]
pub struct JsSourceToJs {
pub source: Either<String, Buffer>,
pub map: Option<String>,
}
impl From<String> for JsSourceToJs {
fn from(source: String) -> Self {
Self {
source: Either::A(source),
map: None,
}
}
}
impl TryFrom<&BoxSource> for JsSourceToJs {
type Error = napi::Error;
fn try_from(value: &BoxSource) -> Result<Self> {
match value.source() {
SourceValue::String(string) => {
let map = value.map(&ObjectPool::default(), &MapOptions::default());
let json = map.map(|m| m.to_json());
Ok(JsSourceToJs {
source: Either::A(string.into_owned()),
map: json,
})
}
SourceValue::Buffer(bytes) => Ok(JsSourceToJs {
source: Either::B(Buffer::from(bytes.to_vec())),
map: None,
}),
}
}
}
impl From<JsSourceToJs> for BoxSource {
fn from(value: JsSourceToJs) -> Self {
match value.source {
Either::A(string) => match value.map {
Some(map) => SourceMapSource::new(WithoutOriginalOptions {
value: string,
name: "inmemory://from js",
#[allow(clippy::unwrap_used)]
source_map: SourceMap::from_json(map).unwrap(),
})
.boxed(),
None => RawStringSource::from(string).boxed(),
},
Either::B(buffer) => RawBufferSource::from(buffer.to_vec()).boxed(),
}
}
}
#[napi(object, object_from_js = false)]
pub struct JsSourceWithLazyMap<'a> {
pub source: Either<&'a str, BufferSlice<'a>>,
#[napi(ts_type = "string | (() => string)")]
pub map: Option<Function<'a, (), String>>,
}
impl JsSourceWithLazyMap<'_> {
pub fn to_js<'a>(env: &'a Env, source: &BoxSource) -> Result<Unknown<'a>> {
let value = source.source();
let binding = match &value {
SourceValue::String(string) => JsSourceWithLazyMap {
source: Either::A(string.as_ref()),
map: LazySourceMap::from_source(source)
.map(|map| {
let pending = Cell::new(Some(map));
env.create_function_from_closure("map", move |_| {
pending
.take()
.ok_or_else(|| napi::Error::from_reason("Source map has already been consumed"))?
.to_json()
})
})
.transpose()?,
},
SourceValue::Buffer(bytes) => JsSourceWithLazyMap {
source: Either::B(BufferSlice::copy_from(env, bytes)?),
map: None,
},
};
binding.into_unknown(env)
}
}
fn has_map_without_generation(source: &dyn Source) -> Option<bool> {
let source = source.as_any();
if source.is::<RawStringSource>() || source.is::<RawBufferSource>() {
return Some(false);
}
if let Some(source) = source.downcast_ref::<OriginalSource>() {
return Some(source.value().bytes().any(|byte| byte != b'\n'));
}
if let Some(source) = source.downcast_ref::<SourceMapSource>() {
return source.inner_source_map().is_none().then_some(true);
}
if let Some(source) = source.downcast_ref::<CachedSource>() {
return has_map_without_generation(source.inner().as_ref());
}
None
}
enum LazySourceMap {
Source(BoxSource),
Map(Box<SourceMap<'static>>),
}
impl LazySourceMap {
fn from_source(source: &BoxSource) -> Option<Self> {
match has_map_without_generation(source.as_ref()) {
Some(false) => None,
Some(true) => Some(Self::Source(Arc::clone(source))),
None => Arc::clone(source)
.map_static(&ObjectPool::default(), &MapOptions::default())
.map(|map| Self::Map(Box::new(map))),
}
}
fn to_json(&self) -> Result<String> {
match self {
Self::Source(source) => Arc::clone(source)
.map_static(&ObjectPool::default(), &MapOptions::default())
.map(|map| map.to_json())
.ok_or_else(|| napi::Error::from_reason("Source map is not available")),
Self::Map(map) => Ok(map.to_json()),
}
}
}