#[cfg(feature = "bundler")]
use at_rule_parser::AtRule;
use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser};
use lightningcss::bundler::BundleErrorKind;
#[cfg(feature = "bundler")]
use lightningcss::bundler::{Bundler, SourceProvider};
use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError};
use lightningcss::dependencies::{Dependency, DependencyOptions};
use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind};
use lightningcss::stylesheet::{
MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet,
};
use lightningcss::targets::{Browsers, Features, Targets};
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use napi::{CallContext, Env, JsObject, JsUnknown};
use parcel_sourcemap::SourceMap;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
mod at_rule_parser;
#[cfg(feature = "bundler")]
#[cfg(not(target_arch = "wasm32"))]
mod threadsafe_function;
#[cfg(feature = "visitor")]
mod transformer;
mod utils;
#[cfg(feature = "visitor")]
use transformer::JsVisitor;
#[cfg(not(feature = "visitor"))]
struct JsVisitor;
#[cfg(feature = "visitor")]
use lightningcss::visitor::Visit;
use utils::get_named_property;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TransformResult<'i> {
#[serde(with = "serde_bytes")]
code: Vec<u8>,
#[serde(with = "serde_bytes")]
map: Option<Vec<u8>>,
exports: Option<CssModuleExports>,
references: Option<CssModuleReferences>,
dependencies: Option<Vec<Dependency>>,
warnings: Vec<Warning<'i>>,
}
impl<'i> TransformResult<'i> {
fn into_js(self, env: Env) -> napi::Result<JsUnknown> {
let mut obj = env.create_object()?;
let buf = env.create_buffer_with_data(self.code)?;
obj.set_named_property("code", buf.into_raw())?;
obj.set_named_property(
"map",
if let Some(map) = self.map {
let buf = env.create_buffer_with_data(map)?;
buf.into_raw().into_unknown()
} else {
env.get_null()?.into_unknown()
},
)?;
obj.set_named_property("exports", env.to_js_value(&self.exports)?)?;
obj.set_named_property("references", env.to_js_value(&self.references)?)?;
obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?;
obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?;
Ok(obj.into_unknown())
}
}
#[cfg(feature = "visitor")]
fn get_visitor(env: Env, opts: &JsObject) -> Option<JsVisitor> {
if let Ok(visitor) = get_named_property::<JsObject>(opts, "visitor") {
Some(JsVisitor::new(env, visitor))
} else {
None
}
}
#[cfg(not(feature = "visitor"))]
fn get_visitor(_env: Env, _opts: &JsObject) -> Option<JsVisitor> {
None
}
pub fn transform(ctx: CallContext) -> napi::Result<JsUnknown> {
let opts = ctx.get::<JsObject>(0)?;
let mut visitor = get_visitor(*ctx.env, &opts);
let config: Config = ctx.env.from_js_value(opts)?;
let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
let res = compile(code, &config, &mut visitor);
match res {
Ok(res) => res.into_js(*ctx.env),
Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
}
}
pub fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
let opts = ctx.get::<JsObject>(0)?;
let mut visitor = get_visitor(*ctx.env, &opts);
let config: AttrConfig = ctx.env.from_js_value(opts)?;
let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
let res = compile_attr(code, &config, &mut visitor);
match res {
Ok(res) => res.into_js(ctx),
Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
}
}
#[cfg(feature = "bundler")]
#[cfg(not(target_arch = "wasm32"))]
mod bundle {
use super::*;
use crossbeam_channel::{self, Receiver, Sender};
use lightningcss::bundler::FileProvider;
use napi::{Env, JsFunction, JsString, NapiRaw};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Mutex;
use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
let opts = ctx.get::<JsObject>(0)?;
let mut visitor = get_visitor(*ctx.env, &opts);
let config: BundleConfig = ctx.env.from_js_value(opts)?;
let fs = FileProvider::new();
fn annotate<'i, 'o, F>(f: F) -> F
where
F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
{
f
}
let res = compile_bundle(
&fs,
&config,
visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
);
match res {
Ok(res) => res.into_js(*ctx.env),
Err(err) => Err(err.into_js_error(*ctx.env, None)?),
}
}
struct JsSourceProvider {
resolve: Option<ThreadsafeFunction<ResolveMessage>>,
read: Option<ThreadsafeFunction<ReadMessage>>,
inputs: Mutex<Vec<*mut String>>,
}
unsafe impl Sync for JsSourceProvider {}
unsafe impl Send for JsSourceProvider {}
thread_local! {
static CHANNEL: (Sender<napi::Result<String>>, Receiver<napi::Result<String>>) = crossbeam_channel::unbounded();
}
impl SourceProvider for JsSourceProvider {
type Error = napi::Error;
fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
let source = if let Some(read) = &self.read {
CHANNEL.with(|channel| {
let message = ReadMessage {
file: file.to_str().unwrap().to_owned(),
tx: channel.0.clone(),
};
read.call(message, ThreadsafeFunctionCallMode::Blocking);
channel.1.recv().unwrap()
})
} else {
Ok(std::fs::read_to_string(file)?)
};
match source {
Ok(source) => {
let ptr = Box::into_raw(Box::new(source));
self.inputs.lock().unwrap().push(ptr);
Ok(unsafe { &*ptr })
}
Err(e) => Err(e),
}
}
fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
if let Some(resolve) = &self.resolve {
return CHANNEL.with(|channel| {
let message = ResolveMessage {
specifier: specifier.to_owned(),
originating_file: originating_file.to_str().unwrap().to_owned(),
tx: channel.0.clone(),
};
resolve.call(message, ThreadsafeFunctionCallMode::Blocking);
let result = channel.1.recv().unwrap();
match result {
Ok(result) => Ok(PathBuf::from_str(&result).unwrap()),
Err(e) => Err(e),
}
});
}
Ok(originating_file.with_file_name(specifier))
}
}
struct ResolveMessage {
specifier: String,
originating_file: String,
tx: Sender<napi::Result<String>>,
}
struct ReadMessage {
file: String,
tx: Sender<napi::Result<String>>,
}
struct VisitMessage {
stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>,
tx: Sender<napi::Result<String>>,
}
fn await_promise(env: Env, result: JsUnknown, tx: Sender<napi::Result<String>>) -> napi::Result<()> {
if result.is_promise()? {
let result: JsObject = result.try_into()?;
let then: JsFunction = get_named_property(&result, "then")?;
let tx2 = tx.clone();
let cb = env.create_function_from_closure("callback", move |ctx| {
let res = ctx.get::<JsString>(0)?.into_utf8()?;
let s = res.into_owned()?;
tx.send(Ok(s)).unwrap();
ctx.env.get_undefined()
})?;
let eb = env.create_function_from_closure("error_callback", move |ctx| {
let res = ctx.get::<JsUnknown>(0)?;
tx2.send(Err(napi::Error::from(res))).unwrap();
ctx.env.get_undefined()
})?;
then.call(Some(&result), &[cb, eb])?;
} else {
let result: JsString = result.try_into()?;
let utf8 = result.into_utf8()?;
let s = utf8.into_owned()?;
tx.send(Ok(s)).unwrap();
}
Ok(())
}
fn resolve_on_js_thread(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
let specifier = ctx.env.create_string(&ctx.value.specifier)?;
let originating_file = ctx.env.create_string(&ctx.value.originating_file)?;
let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?;
await_promise(ctx.env, result, ctx.value.tx)
}
fn handle_error(tx: Sender<napi::Result<String>>, res: napi::Result<()>) -> napi::Result<()> {
match res {
Ok(_) => Ok(()),
Err(e) => {
tx.send(Err(e)).expect("send error");
Ok(())
}
}
}
fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
let tx = ctx.value.tx.clone();
handle_error(tx, resolve_on_js_thread(ctx))
}
fn read_on_js_thread(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
let file = ctx.env.create_string(&ctx.value.file)?;
let result = ctx.callback.unwrap().call(None, &[file])?;
await_promise(ctx.env, result, ctx.value.tx)
}
fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
let tx = ctx.value.tx.clone();
handle_error(tx, read_on_js_thread(ctx))
}
pub fn bundle_async(ctx: CallContext) -> napi::Result<JsObject> {
let opts = ctx.get::<JsObject>(0)?;
let visitor = get_visitor(*ctx.env, &opts);
let config: BundleConfig = ctx.env.from_js_value(&opts)?;
if let Ok(resolver) = get_named_property::<JsObject>(&opts, "resolver") {
let read = if resolver.has_named_property("read")? {
let read = get_named_property::<JsFunction>(&resolver, "read")?;
Some(ThreadsafeFunction::create(
ctx.env.raw(),
unsafe { read.raw() },
0,
read_on_js_thread_wrapper,
)?)
} else {
None
};
let resolve = if resolver.has_named_property("resolve")? {
let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
Some(ThreadsafeFunction::create(
ctx.env.raw(),
unsafe { resolve.raw() },
0,
resolve_on_js_thread_wrapper,
)?)
} else {
None
};
let provider = JsSourceProvider {
resolve,
read,
inputs: Mutex::new(Vec::new()),
};
run_bundle_task(provider, config, visitor, *ctx.env)
} else {
let provider = FileProvider::new();
run_bundle_task(provider, config, visitor, *ctx.env)
}
}
fn run_bundle_task<P: 'static + SourceProvider>(
provider: P,
config: BundleConfig,
visitor: Option<JsVisitor>,
env: Env,
) -> napi::Result<JsObject>
where
P::Error: IntoJsError,
{
let (deferred, promise) = env.create_deferred()?;
let tsfn = if let Some(mut visitor) = visitor {
Some(ThreadsafeFunction::create(
env.raw(),
std::ptr::null_mut(),
0,
move |ctx: ThreadSafeCallContext<VisitMessage>| {
if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) {
ctx.value.tx.send(Err(err)).expect("send error");
return Ok(());
}
ctx.value.tx.send(Ok(Default::default())).expect("send error");
Ok(())
},
)?)
} else {
None
};
rayon::spawn(move || {
let res = compile_bundle(
unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) },
&config,
tsfn.map(move |tsfn| {
move |stylesheet: &mut StyleSheet<AtRule>| {
CHANNEL.with(|channel| {
let message = VisitMessage {
stylesheet: unsafe {
std::mem::transmute::<
&'_ mut StyleSheet<'_, '_, AtRule>,
&'static mut StyleSheet<'static, 'static, AtRule>,
>(stylesheet)
},
tx: channel.0.clone(),
};
tsfn.call(message, ThreadsafeFunctionCallMode::Blocking);
channel.1.recv().expect("recv error").map(|_| ())
})
}
}),
);
deferred.resolve(move |env| match res {
Ok(v) => v.into_js(env),
Err(err) => Err(err.into_js_error(env, None)?),
});
});
Ok(promise)
}
}
#[cfg(feature = "bundler")]
#[cfg(target_arch = "wasm32")]
mod bundle {
use super::*;
use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
use std::cell::UnsafeCell;
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
let opts = ctx.get::<JsObject>(0)?;
let mut visitor = get_visitor(*ctx.env, &opts);
let resolver = get_named_property::<JsObject>(&opts, "resolver")?;
let read = get_named_property::<JsFunction>(&resolver, "read")?;
let resolve = if resolver.has_named_property("resolve")? {
let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
Some(ctx.env.create_reference(resolve)?)
} else {
None
};
let config: BundleConfig = ctx.env.from_js_value(opts)?;
let provider = JsSourceProvider {
env: ctx.env.clone(),
resolve,
read: ctx.env.create_reference(read)?,
inputs: UnsafeCell::new(Vec::new()),
};
fn annotate<'i, 'o, F>(f: F) -> F
where
F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
{
f
}
let res = compile_bundle(
&provider,
&config,
visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
);
match res {
Ok(res) => res.into_js(*ctx.env),
Err(err) => Err(err.into_js_error(*ctx.env, None)?),
}
}
struct JsSourceProvider {
env: Env,
resolve: Option<Ref<()>>,
read: Ref<()>,
inputs: UnsafeCell<Vec<*mut String>>,
}
impl Drop for JsSourceProvider {
fn drop(&mut self) {
if let Some(resolve) = &mut self.resolve {
drop(resolve.unref(self.env));
}
drop(self.read.unref(self.env));
}
}
unsafe impl Sync for JsSourceProvider {}
unsafe impl Send for JsSourceProvider {}
extern "C" {
fn await_promise_sync(
promise: napi::sys::napi_value,
result: *mut napi::sys::napi_value,
error: *mut napi::sys::napi_value,
);
}
fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsString> {
if value.is_promise()? {
let mut result = std::ptr::null_mut();
let mut error = std::ptr::null_mut();
unsafe { await_promise_sync(value.raw(), &mut result, &mut error) };
if !error.is_null() {
let error = unsafe { JsUnknown::from_raw(env.raw(), error)? };
return Err(napi::Error::from(error));
}
if result.is_null() {
return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string()));
}
value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
}
value.try_into()
}
impl SourceProvider for JsSourceProvider {
type Error = napi::Error;
fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?;
let file = self.env.create_string(file.to_str().unwrap())?;
let source: JsUnknown = read.call(None, &[file])?;
let source = get_result(self.env, source)?.into_utf8()?.into_owned()?;
let ptr = Box::into_raw(Box::new(source));
let inputs = unsafe { &mut *self.inputs.get() };
inputs.push(ptr);
Ok(unsafe { &*ptr })
}
fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
if let Some(resolve) = &self.resolve {
let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?;
let specifier = self.env.create_string(specifier)?;
let originating_file = self.env.create_string(originating_file.to_str().unwrap())?;
let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?;
let result = get_result(self.env, result)?.into_utf8()?;
Ok(PathBuf::from_str(result.as_str()?).unwrap())
} else {
Ok(originating_file.with_file_name(specifier))
}
}
}
}
#[cfg(feature = "bundler")]
pub use bundle::*;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Config {
pub filename: Option<String>,
pub project_root: Option<String>,
#[serde(with = "serde_bytes")]
pub code: Vec<u8>,
pub targets: Option<Browsers>,
#[serde(default)]
pub include: u32,
#[serde(default)]
pub exclude: u32,
pub minify: Option<bool>,
pub source_map: Option<bool>,
pub input_source_map: Option<String>,
pub drafts: Option<Drafts>,
pub non_standard: Option<NonStandard>,
pub css_modules: Option<CssModulesOption>,
pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
pub pseudo_classes: Option<OwnedPseudoClasses>,
pub unused_symbols: Option<HashSet<String>>,
pub error_recovery: Option<bool>,
pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum AnalyzeDependenciesOption {
Bool(bool),
Config(AnalyzeDependenciesConfig),
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AnalyzeDependenciesConfig {
preserve_imports: bool,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CssModulesOption {
Bool(bool),
Config(CssModulesConfig),
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CssModulesConfig {
pattern: Option<String>,
dashed_idents: Option<bool>,
}
#[cfg(feature = "bundler")]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BundleConfig {
pub filename: String,
pub project_root: Option<String>,
pub targets: Option<Browsers>,
#[serde(default)]
pub include: u32,
#[serde(default)]
pub exclude: u32,
pub minify: Option<bool>,
pub source_map: Option<bool>,
pub drafts: Option<Drafts>,
pub non_standard: Option<NonStandard>,
pub css_modules: Option<CssModulesOption>,
pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
pub pseudo_classes: Option<OwnedPseudoClasses>,
pub unused_symbols: Option<HashSet<String>>,
pub error_recovery: Option<bool>,
pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OwnedPseudoClasses {
pub hover: Option<String>,
pub active: Option<String>,
pub focus: Option<String>,
pub focus_visible: Option<String>,
pub focus_within: Option<String>,
}
impl<'a> Into<PseudoClasses<'a>> for &'a OwnedPseudoClasses {
fn into(self) -> PseudoClasses<'a> {
PseudoClasses {
hover: self.hover.as_deref(),
active: self.active.as_deref(),
focus: self.focus.as_deref(),
focus_visible: self.focus_visible.as_deref(),
focus_within: self.focus_within.as_deref(),
}
}
}
#[derive(Serialize, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct Drafts {
#[serde(default)]
custom_media: bool,
}
#[derive(Serialize, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct NonStandard {
#[serde(default)]
deep_selector_combinator: bool,
}
fn compile<'i>(
code: &'i str,
config: &Config,
#[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
) -> Result<TransformResult<'i>, CompileError<'i, napi::Error>> {
let drafts = config.drafts.as_ref();
let non_standard = config.non_standard.as_ref();
let warnings = Some(Arc::new(RwLock::new(Vec::new())));
let filename = config.filename.clone().unwrap_or_default();
let project_root = config.project_root.as_ref().map(|p| p.as_ref());
let mut source_map = if config.source_map.unwrap_or_default() {
let mut sm = SourceMap::new(project_root.unwrap_or("/"));
sm.add_source(&filename);
sm.set_source_content(0, code)?;
Some(sm)
} else {
None
};
let res = {
let mut flags = ParserFlags::empty();
flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
flags.set(
ParserFlags::DEEP_SELECTOR_COMBINATOR,
matches!(non_standard, Some(v) if v.deep_selector_combinator),
);
let mut stylesheet = StyleSheet::parse_with(
&code,
ParserOptions {
filename: filename.clone(),
flags,
css_modules: if let Some(css_modules) = &config.css_modules {
match css_modules {
CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
CssModulesOption::Bool(false) => None,
CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
pattern: if let Some(pattern) = c.pattern.as_ref() {
match lightningcss::css_modules::Pattern::parse(pattern) {
Ok(p) => p,
Err(e) => return Err(CompileError::PatternError(e)),
}
} else {
Default::default()
},
dashed_idents: c.dashed_idents.unwrap_or_default(),
}),
}
} else {
None
},
source_index: 0,
error_recovery: config.error_recovery.unwrap_or_default(),
warnings: warnings.clone(),
},
&mut CustomAtRuleParser {
configs: config.custom_at_rules.clone().unwrap_or_default(),
},
)?;
#[cfg(feature = "visitor")]
if let Some(visitor) = visitor.as_mut() {
stylesheet.visit(visitor).map_err(CompileError::JsError)?;
}
let targets = Targets {
browsers: config.targets,
include: Features::from_bits_truncate(config.include),
exclude: Features::from_bits_truncate(config.exclude),
};
stylesheet.minify(MinifyOptions {
targets,
unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
})?;
stylesheet.to_css(PrinterOptions {
minify: config.minify.unwrap_or_default(),
source_map: source_map.as_mut(),
project_root,
targets,
analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
match d {
AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
remove_imports: !c.preserve_imports,
}),
_ => None,
}
} else {
None
},
pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
})?
};
let map = if let Some(mut source_map) = source_map {
if let Some(input_source_map) = &config.input_source_map {
if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) {
let _ = source_map.extends(&mut sm);
}
}
source_map.to_json(None).ok()
} else {
None
};
Ok(TransformResult {
code: res.code.into_bytes(),
map: map.map(|m| m.into_bytes()),
exports: res.exports,
references: res.references,
dependencies: res.dependencies,
warnings: warnings.map_or(Vec::new(), |w| {
Arc::try_unwrap(w)
.unwrap()
.into_inner()
.unwrap()
.into_iter()
.map(|w| w.into())
.collect()
}),
})
}
#[cfg(feature = "bundler")]
fn compile_bundle<
'i,
'o,
P: SourceProvider,
F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
>(
fs: &'i P,
config: &'o BundleConfig,
visit: Option<F>,
) -> Result<TransformResult<'i>, CompileError<'i, P::Error>> {
use std::path::Path;
let project_root = config.project_root.as_ref().map(|p| p.as_ref());
let mut source_map = if config.source_map.unwrap_or_default() {
Some(SourceMap::new(project_root.unwrap_or("/")))
} else {
None
};
let warnings = Some(Arc::new(RwLock::new(Vec::new())));
let res = {
let drafts = config.drafts.as_ref();
let non_standard = config.non_standard.as_ref();
let mut flags = ParserFlags::empty();
flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
flags.set(
ParserFlags::DEEP_SELECTOR_COMBINATOR,
matches!(non_standard, Some(v) if v.deep_selector_combinator),
);
let parser_options = ParserOptions {
flags,
css_modules: if let Some(css_modules) = &config.css_modules {
match css_modules {
CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
CssModulesOption::Bool(false) => None,
CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
pattern: if let Some(pattern) = c.pattern.as_ref() {
match lightningcss::css_modules::Pattern::parse(pattern) {
Ok(p) => p,
Err(e) => return Err(CompileError::PatternError(e)),
}
} else {
Default::default()
},
dashed_idents: c.dashed_idents.unwrap_or_default(),
}),
}
} else {
None
},
error_recovery: config.error_recovery.unwrap_or_default(),
warnings: warnings.clone(),
filename: String::new(),
source_index: 0,
};
let mut at_rule_parser = CustomAtRuleParser {
configs: config.custom_at_rules.clone().unwrap_or_default(),
};
let mut bundler =
Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser);
let mut stylesheet = bundler.bundle(Path::new(&config.filename))?;
if let Some(visit) = visit {
visit(&mut stylesheet).map_err(CompileError::JsError)?;
}
let targets = Targets {
browsers: config.targets,
include: Features::from_bits_truncate(config.include),
exclude: Features::from_bits_truncate(config.exclude),
};
stylesheet.minify(MinifyOptions {
targets,
unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
})?;
stylesheet.to_css(PrinterOptions {
minify: config.minify.unwrap_or_default(),
source_map: source_map.as_mut(),
project_root,
targets,
analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
match d {
AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
remove_imports: !c.preserve_imports,
}),
_ => None,
}
} else {
None
},
pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
})?
};
let map = if let Some(source_map) = &mut source_map {
source_map.to_json(None).ok()
} else {
None
};
Ok(TransformResult {
code: res.code.into_bytes(),
map: map.map(|m| m.into_bytes()),
exports: res.exports,
references: res.references,
dependencies: res.dependencies,
warnings: warnings.map_or(Vec::new(), |w| {
Arc::try_unwrap(w)
.unwrap()
.into_inner()
.unwrap()
.into_iter()
.map(|w| w.into())
.collect()
}),
})
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AttrConfig {
pub filename: Option<String>,
#[serde(with = "serde_bytes")]
pub code: Vec<u8>,
pub targets: Option<Browsers>,
#[serde(default)]
pub include: u32,
#[serde(default)]
pub exclude: u32,
#[serde(default)]
pub minify: bool,
#[serde(default)]
pub analyze_dependencies: bool,
#[serde(default)]
pub error_recovery: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AttrResult<'i> {
#[serde(with = "serde_bytes")]
code: Vec<u8>,
dependencies: Option<Vec<Dependency>>,
warnings: Vec<Warning<'i>>,
}
impl<'i> AttrResult<'i> {
fn into_js(self, ctx: CallContext) -> napi::Result<JsUnknown> {
let mut obj = ctx.env.create_object()?;
let buf = ctx.env.create_buffer_with_data(self.code)?;
obj.set_named_property("code", buf.into_raw())?;
obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?;
obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?;
Ok(obj.into_unknown())
}
}
fn compile_attr<'i>(
code: &'i str,
config: &AttrConfig,
#[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
) -> Result<AttrResult<'i>, CompileError<'i, napi::Error>> {
let warnings = if config.error_recovery {
Some(Arc::new(RwLock::new(Vec::new())))
} else {
None
};
let res = {
let filename = config.filename.clone().unwrap_or_default();
let mut attr = StyleAttribute::parse(
&code,
ParserOptions {
filename,
error_recovery: config.error_recovery,
warnings: warnings.clone(),
..ParserOptions::default()
},
)?;
#[cfg(feature = "visitor")]
if let Some(visitor) = visitor.as_mut() {
attr.visit(visitor).unwrap();
}
let targets = Targets {
browsers: config.targets,
include: Features::from_bits_truncate(config.include),
exclude: Features::from_bits_truncate(config.exclude),
};
attr.minify(MinifyOptions {
targets,
..MinifyOptions::default()
});
attr.to_css(PrinterOptions {
minify: config.minify,
source_map: None,
project_root: None,
targets,
analyze_dependencies: if config.analyze_dependencies {
Some(DependencyOptions::default())
} else {
None
},
pseudo_classes: None,
})?
};
Ok(AttrResult {
code: res.code.into_bytes(),
dependencies: res.dependencies,
warnings: warnings.map_or(Vec::new(), |w| {
Arc::try_unwrap(w)
.unwrap()
.into_inner()
.unwrap()
.into_iter()
.map(|w| w.into())
.collect()
}),
})
}
enum CompileError<'i, E: std::error::Error> {
ParseError(Error<ParserError<'i>>),
MinifyError(Error<MinifyErrorKind>),
PrinterError(Error<PrinterErrorKind>),
SourceMapError(parcel_sourcemap::SourceMapError),
BundleError(Error<BundleErrorKind<'i, E>>),
PatternError(PatternParseError),
#[cfg(feature = "visitor")]
JsError(napi::Error),
}
impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CompileError::ParseError(err) => err.kind.fmt(f),
CompileError::MinifyError(err) => err.kind.fmt(f),
CompileError::PrinterError(err) => err.kind.fmt(f),
CompileError::BundleError(err) => err.kind.fmt(f),
CompileError::PatternError(err) => err.fmt(f),
CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), #[cfg(feature = "visitor")]
CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f),
}
}
}
impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> {
fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result<napi::Error> {
let reason = self.to_string();
let data = match &self {
CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?,
CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?,
CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?,
CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?,
_ => env.get_null()?.into_unknown(),
};
let (js_error, loc) = match self {
CompileError::BundleError(Error {
loc,
kind: BundleErrorKind::ResolverError(e),
}) => {
(e.into_js_error(env)?, loc)
}
CompileError::ParseError(Error { loc, .. })
| CompileError::PrinterError(Error { loc, .. })
| CompileError::MinifyError(Error { loc, .. })
| CompileError::BundleError(Error { loc, .. }) => {
let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
let reason = env.create_string_from_std(reason)?;
let obj = syntax_error.new_instance(&[reason])?;
(obj.into_unknown(), loc)
}
_ => return Ok(self.into()),
};
if js_error.get_type()? == napi::ValueType::Object {
let mut obj: JsObject = unsafe { js_error.cast() };
if let Some(loc) = loc {
let line = env.create_int32((loc.line + 1) as i32)?;
let col = env.create_int32(loc.column as i32)?;
let filename = env.create_string_from_std(loc.filename)?;
obj.set_named_property("fileName", filename)?;
if let Some(code) = code {
let source = env.create_string(code)?;
obj.set_named_property("source", source)?;
}
let mut loc = env.create_object()?;
loc.set_named_property("line", line)?;
loc.set_named_property("column", col)?;
obj.set_named_property("loc", loc)?;
}
obj.set_named_property("data", data)?;
Ok(obj.into_unknown().into())
} else {
Ok(js_error.into())
}
}
}
trait IntoJsError {
fn into_js_error(self, env: Env) -> napi::Result<JsUnknown>;
}
impl IntoJsError for std::io::Error {
fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
let reason = self.to_string();
let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
let reason = env.create_string_from_std(reason)?;
let obj = syntax_error.new_instance(&[reason])?;
Ok(obj.into_unknown())
}
}
impl IntoJsError for napi::Error {
fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) }
}
}
impl<'i, E: std::error::Error> From<Error<ParserError<'i>>> for CompileError<'i, E> {
fn from(e: Error<ParserError<'i>>) -> CompileError<'i, E> {
CompileError::ParseError(e)
}
}
impl<'i, E: std::error::Error> From<Error<MinifyErrorKind>> for CompileError<'i, E> {
fn from(err: Error<MinifyErrorKind>) -> CompileError<'i, E> {
CompileError::MinifyError(err)
}
}
impl<'i, E: std::error::Error> From<Error<PrinterErrorKind>> for CompileError<'i, E> {
fn from(err: Error<PrinterErrorKind>) -> CompileError<'i, E> {
CompileError::PrinterError(err)
}
}
impl<'i, E: std::error::Error> From<parcel_sourcemap::SourceMapError> for CompileError<'i, E> {
fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> {
CompileError::SourceMapError(e)
}
}
impl<'i, E: std::error::Error> From<Error<BundleErrorKind<'i, E>>> for CompileError<'i, E> {
fn from(e: Error<BundleErrorKind<'i, E>>) -> CompileError<'i, E> {
CompileError::BundleError(e)
}
}
impl<'i, E: std::error::Error> From<CompileError<'i, E>> for napi::Error {
fn from(e: CompileError<'i, E>) -> napi::Error {
match e {
CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()),
CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()),
#[cfg(feature = "visitor")]
CompileError::JsError(e) => e,
_ => napi::Error::new(napi::Status::GenericFailure, e.to_string()),
}
}
}
#[derive(Serialize)]
struct Warning<'i> {
message: String,
#[serde(flatten)]
data: ParserError<'i>,
loc: Option<ErrorLocation>,
}
impl<'i> From<Error<ParserError<'i>>> for Warning<'i> {
fn from(mut e: Error<ParserError<'i>>) -> Self {
if let Some(loc) = &mut e.loc {
loc.line += 1;
}
Warning {
message: e.kind.to_string(),
data: e.kind,
loc: e.loc,
}
}
}