use clap::{ArgGroup, Parser};
use lightningcss::bundler::{Bundler, FileProvider};
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet};
use lightningcss::targets::Browsers;
use parcel_sourcemap::SourceMap;
use serde::Serialize;
use std::sync::{Arc, RwLock};
use std::{ffi, fs, io, path, path::Path};
#[cfg(target_os = "macos")]
#[global_allocator]
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[clap(group(
ArgGroup::new("targets-resolution")
.args(&["targets", "browserslist"]),
))]
struct CliArgs {
#[clap(value_parser)]
input_file: String,
#[clap(short, long, group = "output_file", value_parser)]
output_file: Option<String>,
#[clap(short, long, value_parser)]
minify: bool,
#[clap(long, value_parser)]
nesting: bool,
#[clap(long, value_parser)]
custom_media: bool,
#[clap(long, group = "css_modules", value_parser)]
css_modules: Option<Option<String>>,
#[clap(long, requires = "css_modules", value_parser)]
css_modules_pattern: Option<String>,
#[clap(long, requires = "css_modules", value_parser)]
css_modules_dashed_idents: bool,
#[clap(long, requires = "output_file", value_parser)]
sourcemap: bool,
#[clap(long, value_parser)]
bundle: bool,
#[clap(short, long, value_parser)]
targets: Vec<String>,
#[clap(long, value_parser)]
browserslist: bool,
#[clap(long, value_parser)]
error_recovery: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SourceMapJson<'a> {
version: u8,
mappings: String,
sources: &'a Vec<String>,
sources_content: &'a Vec<String>,
names: &'a Vec<String>,
}
pub fn main() -> Result<(), std::io::Error> {
let cli_args = CliArgs::parse();
let source = fs::read_to_string(&cli_args.input_file)?;
let absolute_path = fs::canonicalize(&cli_args.input_file)?;
let filename = pathdiff::diff_paths(absolute_path, std::env::current_dir()?).unwrap();
let filename = filename.to_str().unwrap();
let css_modules = if let Some(_) = cli_args.css_modules {
let pattern = if let Some(pattern) = cli_args.css_modules_pattern.as_ref() {
match lightningcss::css_modules::Pattern::parse(pattern) {
Ok(p) => p,
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
}
} else {
Default::default()
};
Some(lightningcss::css_modules::Config {
pattern,
dashed_idents: cli_args.css_modules_dashed_idents,
..Default::default()
})
} else {
cli_args.css_modules.as_ref().map(|_| Default::default())
};
let fs = FileProvider::new();
let warnings = if cli_args.error_recovery {
Some(Arc::new(RwLock::new(Vec::new())))
} else {
None
};
let mut source_map = if cli_args.sourcemap {
Some(SourceMap::new("/"))
} else {
None
};
let res = {
let mut options = ParserOptions {
nesting: cli_args.nesting,
css_modules,
custom_media: cli_args.custom_media,
error_recovery: cli_args.error_recovery,
warnings: warnings.clone(),
..ParserOptions::default()
};
let mut stylesheet = if cli_args.bundle {
let mut bundler = Bundler::new(&fs, source_map.as_mut(), options);
bundler.bundle(Path::new(&cli_args.input_file)).unwrap()
} else {
if let Some(sm) = &mut source_map {
sm.add_source(&filename);
let _ = sm.set_source_content(0, &source);
}
options.filename = filename.to_owned();
StyleSheet::parse(&source, options).unwrap()
};
let targets = if !cli_args.targets.is_empty() {
Browsers::from_browserslist(cli_args.targets).unwrap()
} else if cli_args.browserslist {
Browsers::load_browserslist().unwrap()
} else {
None
};
stylesheet
.minify(MinifyOptions {
targets,
..MinifyOptions::default()
})
.unwrap();
stylesheet
.to_css(PrinterOptions {
minify: cli_args.minify,
source_map: source_map.as_mut(),
targets,
..PrinterOptions::default()
})
.unwrap()
};
let map = if let Some(ref mut source_map) = source_map {
let mut vlq_output: Vec<u8> = Vec::new();
source_map
.write_vlq(&mut vlq_output)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Error writing sourcemap vlq"))?;
let sm = SourceMapJson {
version: 3,
mappings: unsafe { String::from_utf8_unchecked(vlq_output) },
sources: source_map.get_sources(),
sources_content: source_map.get_sources_content(),
names: source_map.get_names(),
};
serde_json::to_vec(&sm).ok()
} else {
None
};
if let Some(warnings) = warnings {
let warnings = Arc::try_unwrap(warnings).unwrap().into_inner().unwrap();
for warning in warnings {
eprintln!("{}", warning);
}
}
if let Some(output_file) = &cli_args.output_file {
let mut code = res.code;
if cli_args.sourcemap {
if let Some(map_buf) = map {
let map_filename: String = output_file.to_owned() + ".map";
code += &format!("\n/*# sourceMappingURL={} */\n", map_filename);
fs::write(map_filename, map_buf)?;
}
}
fs::write(output_file, code.as_bytes())?;
if let Some(css_modules) = cli_args.css_modules {
let css_modules_filename = if let Some(name) = css_modules {
name
} else {
infer_css_modules_filename(&output_file)?
};
if let Some(exports) = res.exports {
let css_modules_json = serde_json::to_string(&exports)?;
fs::write(css_modules_filename, css_modules_json)?;
}
}
} else {
if let Some(exports) = res.exports {
println!(
"{}",
serde_json::json!({
"code": res.code,
"exports": exports
})
);
} else {
println!("{}", res.code);
}
}
Ok(())
}
fn infer_css_modules_filename(output_file: &str) -> Result<String, std::io::Error> {
let path = path::Path::new(output_file);
if path.extension() == Some(ffi::OsStr::new("json")) {
Err(io::Error::new(
io::ErrorKind::Other,
"Cannot infer a css modules json filename, since the output file extension is '.json'",
))
} else {
Ok(path.with_extension("json").to_str().unwrap().into())
}
}