Skip to content

Commit

Permalink
Add support for processing multiple files at once in CLI
Browse files Browse the repository at this point in the history
Closes #90. Closes #434.
  • Loading branch information
devongovett committed Apr 19, 2023
1 parent 15a48a8 commit 9231a5a
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 123 deletions.
272 changes: 150 additions & 122 deletions src/main.rs
Expand Up @@ -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]
Expand All @@ -21,10 +22,13 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
struct CliArgs {
/// Target CSS file (default: stdin)
#[clap(value_parser)]
input_file: Option<String>,
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,
Expand Down Expand Up @@ -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::<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 {
Expand All @@ -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<u8> = 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<u8> = 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<String, std::io::Error> {
let path = path::Path::new(output_file);
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,
Expand Down

0 comments on commit 9231a5a

Please sign in to comment.