| use atty::Stream; |
| use clap::{ArgGroup, Parser}; |
| use lightningcss::bundler::{Bundler, FileProvider}; |
| use lightningcss::stylesheet::{MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, StyleSheet}; |
| use lightningcss::targets::Browsers; |
| use parcel_sourcemap::SourceMap; |
| use serde::Serialize; |
| use std::borrow::Cow; |
| use std::sync::{Arc, RwLock}; |
| use std::{ffi, fs, io, 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 { |
| /// Target CSS file (default: stdin) |
| #[clap(value_parser)] |
| input_file: Vec<String>, |
| /// Destination file for the output |
| #[clap(short, long, group = "output_file", value_parser)] |
| output_file: Option<String>, |
| /// Destination directory to output into. |
| #[clap(short = 'd', long, group = "output_file", value_parser)] |
| output_dir: Option<String>, |
| /// Minify the output |
| #[clap(short, long, value_parser)] |
| minify: bool, |
| /// Enable parsing CSS nesting |
| // Now on by default, but left for backward compatibility. |
| #[clap(long, value_parser, hide = true)] |
| nesting: bool, |
| /// Enable parsing custom media queries |
| #[clap(long, value_parser)] |
| custom_media: bool, |
| /// Enable CSS modules in output. |
| /// If no filename is provided, <output_file>.json will be used. |
| /// If no --output-file is specified, code and exports will be printed to stdout as JSON. |
| #[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, |
| /// Enable sourcemap, at <output_file>.map |
| #[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 project_root = std::env::current_dir()?; |
| |
| // If we're given an input file, read from it and adjust its name. |
| // |
| // If we're not given an input file and stdin was redirected, read |
| // from it and create a fake name. Return an error if stdin was not |
| // redirected (otherwise the program will hang waiting for input). |
| // |
| let inputs = if !cli_args.input_file.is_empty() { |
| if cli_args.input_file.len() > 1 && cli_args.output_file.is_some() { |
| eprintln!("Cannot use the --output-file option with multiple inputs. Use --output-dir instead."); |
| std::process::exit(1); |
| } |
| |
| if cli_args.input_file.len() > 1 && cli_args.output_file.is_none() && cli_args.output_dir.is_none() { |
| eprintln!("Cannot output to stdout with multiple inputs. Use --output-dir instead."); |
| std::process::exit(1); |
| } |
| |
| cli_args |
| .input_file |
| .into_iter() |
| .map(|ref f| -> Result<_, std::io::Error> { |
| let absolute_path = fs::canonicalize(f)?; |
| let filename = pathdiff::diff_paths(absolute_path, &project_root).unwrap(); |
| let filename = filename.to_string_lossy().into_owned(); |
| let contents = fs::read_to_string(f)?; |
| Ok((filename, contents)) |
| }) |
| .collect::<Result<_, _>>()? |
| } else { |
| // Don't silently wait for input if stdin was not redirected. |
| if atty::is(Stream::Stdin) { |
| return Err(io::Error::new( |
| io::ErrorKind::Other, |
| "Not reading from stdin as it was not redirected", |
| )); |
| } |
| let filename = format!("stdin-{}", std::process::id()); |
| let contents = io::read_to_string(io::stdin())?; |
| vec![(filename, contents)] |
| }; |
| |
| 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(); |
| |
| for (filename, source) in inputs { |
| 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(&project_root.to_string_lossy())) |
| } else { |
| None |
| }; |
| |
| let output_file = if let Some(output_file) = &cli_args.output_file { |
| Some(Cow::Borrowed(Path::new(output_file))) |
| } else if let Some(dir) = &cli_args.output_dir { |
| Some(Cow::Owned( |
| Path::new(dir).join(Path::new(&filename).file_name().unwrap()), |
| )) |
| } else { |
| None |
| }; |
| |
| let res = { |
| let mut flags = ParserFlags::empty(); |
| flags.set(ParserFlags::CUSTOM_MEDIA, cli_args.custom_media); |
| |
| let mut options = ParserOptions { |
| flags, |
| css_modules: css_modules.clone(), |
| 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(&filename)).unwrap() |
| } else { |
| if let Some(sm) = &mut source_map { |
| sm.add_source(&filename); |
| let _ = sm.set_source_content(0, &source); |
| } |
| options.filename = filename; |
| 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 |
| } |
| .into(); |
| |
| stylesheet |
| .minify(MinifyOptions { |
| targets, |
| ..MinifyOptions::default() |
| }) |
| .unwrap(); |
| |
| stylesheet |
| .to_css(PrinterOptions { |
| minify: cli_args.minify, |
| source_map: source_map.as_mut(), |
| project_root: Some(&project_root.to_string_lossy()), |
| 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) = &output_file { |
| let mut code = res.code; |
| if cli_args.sourcemap { |
| if let Some(map_buf) = map { |
| let map_filename = output_file.to_string_lossy() + ".map"; |
| code += &format!("\n/*# sourceMappingURL={} */\n", map_filename); |
| fs::write(map_filename.as_ref(), map_buf)?; |
| } |
| } |
| |
| if let Some(p) = output_file.parent() { |
| fs::create_dir_all(p)? |
| }; |
| 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 { |
| Cow::Borrowed(name) |
| } else { |
| Cow::Owned(infer_css_modules_filename(output_file.as_ref())?) |
| }; |
| if let Some(exports) = res.exports { |
| let css_modules_json = serde_json::to_string(&exports)?; |
| fs::write(css_modules_filename.as_ref(), 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(path: &Path) -> Result<String, std::io::Error> { |
| 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 { |
| // unwrap: the filename option is a String from clap, so is valid utf-8 |
| Ok(path.with_extension("json").to_str().unwrap().into()) |
| } |
| } |