diff --git a/src/main.rs b/src/main.rs index 363d3f08..7d85ce0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,9 @@ use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, Sty 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::Path}; +use std::{ffi, fs, io, path::Path}; #[cfg(target_os = "macos")] #[global_allocator] @@ -21,10 +22,13 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; struct CliArgs { /// Target CSS file (default: stdin) #[clap(value_parser)] - input_file: Option, + input_file: Vec, /// Destination file for the output #[clap(short, long, group = "output_file", value_parser)] output_file: Option, + /// Destination directory to output into. + #[clap(short = 'd', long, group = "output_file", value_parser)] + output_dir: Option, /// Minify the output #[clap(short, long, value_parser)] minify: bool, @@ -76,26 +80,39 @@ pub fn main() -> Result<(), std::io::Error> { // from it and create a fake name. Return an error if stdin was not // redirected (otherwise the program will hang waiting for input). // - let (filename, source) = match &cli_args.input_file { - Some(f) => { - 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)?; - (filename, contents) + 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); } - None => { - // 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())?; - (filename, contents) + + 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::>()? + } 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 { @@ -121,138 +138,149 @@ pub fn main() -> Result<(), std::io::Error> { }; 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(&project_root.to_string_lossy())) - } 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() + for (filename, source) in inputs { + let warnings = if cli_args.error_recovery { + Some(Arc::new(RwLock::new(Vec::new()))) + } else { + None }; - let mut stylesheet = if cli_args.bundle { - let mut bundler = Bundler::new(&fs, source_map.as_mut(), options); - bundler.bundle(Path::new(&filename)).unwrap() + let mut source_map = if cli_args.sourcemap { + Some(SourceMap::new(&project_root.to_string_lossy())) } 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() + None }; - let targets = if !cli_args.targets.is_empty() { - Browsers::from_browserslist(cli_args.targets).unwrap() - } else if cli_args.browserslist { - Browsers::load_browserslist().unwrap() + 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 }; - stylesheet - .minify(MinifyOptions { - targets, - ..MinifyOptions::default() - }) - .unwrap(); + let res = { + let mut options = ParserOptions { + nesting: cli_args.nesting, + css_modules: css_modules.clone(), + custom_media: cli_args.custom_media, + error_recovery: cli_args.error_recovery, + warnings: warnings.clone(), + ..ParserOptions::default() + }; - 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 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 map = if let Some(ref mut source_map) = source_map { - let mut vlq_output: Vec = Vec::new(); - source_map - .write_vlq(&mut vlq_output) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "Error writing sourcemap vlq"))?; + 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 + }; - 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(), + 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() }; - serde_json::to_vec(&sm).ok() - } else { - None - }; + let map = if let Some(ref mut source_map) = source_map { + let mut vlq_output: Vec = Vec::new(); + source_map + .write_vlq(&mut vlq_output) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "Error writing sourcemap vlq"))?; - if let Some(warnings) = warnings { - let warnings = Arc::try_unwrap(warnings).unwrap().into_inner().unwrap(); - for warning in warnings { - eprintln!("{}", warning); - } - } + 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(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)?; + if let Some(warnings) = warnings { + let warnings = Arc::try_unwrap(warnings).unwrap().into_inner().unwrap(); + for warning in warnings { + eprintln!("{}", warning); } } - let output_path = Path::new(output_file); - if let Some(p) = output_path.parent() { - fs::create_dir_all(p)? - }; - fs::write(output_file, code.as_bytes())?; + 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(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(p) = output_file.parent() { + fs::create_dir_all(p)? }; - if let Some(exports) = res.exports { - let css_modules_json = serde_json::to_string(&exports)?; - fs::write(css_modules_filename, css_modules_json)?; + 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); + 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 { - let path = path::Path::new(output_file); +fn infer_css_modules_filename(path: &Path) -> Result { if path.extension() == Some(ffi::OsStr::new("json")) { Err(io::Error::new( io::ErrorKind::Other, diff --git a/tests/cli_integration_tests.rs b/tests/cli_integration_tests.rs index cdb7af96..1aeba30f 100644 --- a/tests/cli_integration_tests.rs +++ b/tests/cli_integration_tests.rs @@ -20,6 +20,18 @@ fn test_file() -> Result { Ok(file) } +fn test_file2() -> Result { + let file = assert_fs::NamedTempFile::new("test2.css")?; + file.write_str( + r#" + .foo { + color: yellow; + } + "#, + )?; + Ok(file) +} + fn css_module_test_vals() -> (String, String, String) { let exports: HashMap<&str, CssModuleExport> = HashMap::from([ ( @@ -68,7 +80,7 @@ fn css_module_test_vals() -> (String, String, String) { .foo { color: red; } - + #id { animation: 2s test; } @@ -205,6 +217,59 @@ fn output_file_option_create_missing_directories() -> Result<(), Box Result<(), Box> { + let infile = test_file()?; + let infile2 = test_file2()?; + let outdir = assert_fs::TempDir::new()?; + let mut cmd = Command::cargo_bin("lightningcss")?; + cmd.arg(infile.path()); + cmd.arg(infile2.path()); + cmd.arg("--output-dir").arg(outdir.path()); + cmd.assert().success(); + outdir + .child(infile.file_name().unwrap()) + .assert(predicate::str::contains(indoc! {r#" + .foo { + border: none; + }"#})); + outdir + .child(infile2.file_name().unwrap()) + .assert(predicate::str::contains(indoc! {r#" + .foo { + color: #ff0; + }"#})); + + Ok(()) +} + +#[test] +fn multiple_input_files_out_file() -> Result<(), Box> { + let infile = test_file()?; + let infile2 = test_file2()?; + let outdir = assert_fs::TempDir::new()?; + let mut cmd = Command::cargo_bin("lightningcss")?; + cmd.arg(infile.path()); + cmd.arg(infile2.path()); + cmd.arg("--output-file").arg(outdir.path()); + cmd.assert().failure(); + + Ok(()) +} + +#[test] +fn multiple_input_files_stdout() -> Result<(), Box> { + let infile = test_file()?; + let infile2 = test_file2()?; + let outdir = assert_fs::TempDir::new()?; + let mut cmd = Command::cargo_bin("lightningcss")?; + cmd.arg(infile.path()); + cmd.arg(infile2.path()); + cmd.assert().failure(); + + Ok(()) +} + #[test] fn minify_option() -> Result<(), Box> { let infile = test_file()?;