Skip to content

Commit 9231a5a

Browse files
committed
Add support for processing multiple files at once in CLI
Closes parcel-bundler#90. Closes parcel-bundler#434.
1 parent 15a48a8 commit 9231a5a

File tree

2 files changed

+216
-123
lines changed

2 files changed

+216
-123
lines changed

src/main.rs

Lines changed: 150 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, Sty
55
use lightningcss::targets::Browsers;
66
use parcel_sourcemap::SourceMap;
77
use serde::Serialize;
8+
use std::borrow::Cow;
89
use std::sync::{Arc, RwLock};
9-
use std::{ffi, fs, io, path, path::Path};
10+
use std::{ffi, fs, io, path::Path};
1011

1112
#[cfg(target_os = "macos")]
1213
#[global_allocator]
@@ -21,10 +22,13 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
2122
struct CliArgs {
2223
/// Target CSS file (default: stdin)
2324
#[clap(value_parser)]
24-
input_file: Option<String>,
25+
input_file: Vec<String>,
2526
/// Destination file for the output
2627
#[clap(short, long, group = "output_file", value_parser)]
2728
output_file: Option<String>,
29+
/// Destination directory to output into.
30+
#[clap(short = 'd', long, group = "output_file", value_parser)]
31+
output_dir: Option<String>,
2832
/// Minify the output
2933
#[clap(short, long, value_parser)]
3034
minify: bool,
@@ -76,26 +80,39 @@ pub fn main() -> Result<(), std::io::Error> {
7680
// from it and create a fake name. Return an error if stdin was not
7781
// redirected (otherwise the program will hang waiting for input).
7882
//
79-
let (filename, source) = match &cli_args.input_file {
80-
Some(f) => {
81-
let absolute_path = fs::canonicalize(f)?;
82-
let filename = pathdiff::diff_paths(absolute_path, &project_root).unwrap();
83-
let filename = filename.to_string_lossy().into_owned();
84-
let contents = fs::read_to_string(f)?;
85-
(filename, contents)
83+
let inputs = if !cli_args.input_file.is_empty() {
84+
if cli_args.input_file.len() > 1 && cli_args.output_file.is_some() {
85+
eprintln!("Cannot use the --output-file option with multiple inputs. Use --output-dir instead.");
86+
std::process::exit(1);
8687
}
87-
None => {
88-
// Don't silently wait for input if stdin was not redirected.
89-
if atty::is(Stream::Stdin) {
90-
return Err(io::Error::new(
91-
io::ErrorKind::Other,
92-
"Not reading from stdin as it was not redirected",
93-
));
94-
}
95-
let filename = format!("stdin-{}", std::process::id());
96-
let contents = io::read_to_string(io::stdin())?;
97-
(filename, contents)
88+
89+
if cli_args.input_file.len() > 1 && cli_args.output_file.is_none() && cli_args.output_dir.is_none() {
90+
eprintln!("Cannot output to stdout with multiple inputs. Use --output-dir instead.");
91+
std::process::exit(1);
9892
}
93+
94+
cli_args
95+
.input_file
96+
.into_iter()
97+
.map(|ref f| -> Result<_, std::io::Error> {
98+
let absolute_path = fs::canonicalize(f)?;
99+
let filename = pathdiff::diff_paths(absolute_path, &project_root).unwrap();
100+
let filename = filename.to_string_lossy().into_owned();
101+
let contents = fs::read_to_string(f)?;
102+
Ok((filename, contents))
103+
})
104+
.collect::<Result<_, _>>()?
105+
} else {
106+
// Don't silently wait for input if stdin was not redirected.
107+
if atty::is(Stream::Stdin) {
108+
return Err(io::Error::new(
109+
io::ErrorKind::Other,
110+
"Not reading from stdin as it was not redirected",
111+
));
112+
}
113+
let filename = format!("stdin-{}", std::process::id());
114+
let contents = io::read_to_string(io::stdin())?;
115+
vec![(filename, contents)]
99116
};
100117

101118
let css_modules = if let Some(_) = cli_args.css_modules {
@@ -121,138 +138,149 @@ pub fn main() -> Result<(), std::io::Error> {
121138
};
122139

123140
let fs = FileProvider::new();
124-
let warnings = if cli_args.error_recovery {
125-
Some(Arc::new(RwLock::new(Vec::new())))
126-
} else {
127-
None
128-
};
129-
130-
let mut source_map = if cli_args.sourcemap {
131-
Some(SourceMap::new(&project_root.to_string_lossy()))
132-
} else {
133-
None
134-
};
135141

136-
let res = {
137-
let mut options = ParserOptions {
138-
nesting: cli_args.nesting,
139-
css_modules,
140-
custom_media: cli_args.custom_media,
141-
error_recovery: cli_args.error_recovery,
142-
warnings: warnings.clone(),
143-
..ParserOptions::default()
142+
for (filename, source) in inputs {
143+
let warnings = if cli_args.error_recovery {
144+
Some(Arc::new(RwLock::new(Vec::new())))
145+
} else {
146+
None
144147
};
145148

146-
let mut stylesheet = if cli_args.bundle {
147-
let mut bundler = Bundler::new(&fs, source_map.as_mut(), options);
148-
bundler.bundle(Path::new(&filename)).unwrap()
149+
let mut source_map = if cli_args.sourcemap {
150+
Some(SourceMap::new(&project_root.to_string_lossy()))
149151
} else {
150-
if let Some(sm) = &mut source_map {
151-
sm.add_source(&filename);
152-
let _ = sm.set_source_content(0, &source);
153-
}
154-
options.filename = filename;
155-
StyleSheet::parse(&source, options).unwrap()
152+
None
156153
};
157154

158-
let targets = if !cli_args.targets.is_empty() {
159-
Browsers::from_browserslist(cli_args.targets).unwrap()
160-
} else if cli_args.browserslist {
161-
Browsers::load_browserslist().unwrap()
155+
let output_file = if let Some(output_file) = &cli_args.output_file {
156+
Some(Cow::Borrowed(Path::new(output_file)))
157+
} else if let Some(dir) = &cli_args.output_dir {
158+
Some(Cow::Owned(
159+
Path::new(dir).join(Path::new(&filename).file_name().unwrap()),
160+
))
162161
} else {
163162
None
164163
};
165164

166-
stylesheet
167-
.minify(MinifyOptions {
168-
targets,
169-
..MinifyOptions::default()
170-
})
171-
.unwrap();
165+
let res = {
166+
let mut options = ParserOptions {
167+
nesting: cli_args.nesting,
168+
css_modules: css_modules.clone(),
169+
custom_media: cli_args.custom_media,
170+
error_recovery: cli_args.error_recovery,
171+
warnings: warnings.clone(),
172+
..ParserOptions::default()
173+
};
172174

173-
stylesheet
174-
.to_css(PrinterOptions {
175-
minify: cli_args.minify,
176-
source_map: source_map.as_mut(),
177-
project_root: Some(&project_root.to_string_lossy()),
178-
targets,
179-
..PrinterOptions::default()
180-
})
181-
.unwrap()
182-
};
175+
let mut stylesheet = if cli_args.bundle {
176+
let mut bundler = Bundler::new(&fs, source_map.as_mut(), options);
177+
bundler.bundle(Path::new(&filename)).unwrap()
178+
} else {
179+
if let Some(sm) = &mut source_map {
180+
sm.add_source(&filename);
181+
let _ = sm.set_source_content(0, &source);
182+
}
183+
options.filename = filename;
184+
StyleSheet::parse(&source, options).unwrap()
185+
};
183186

184-
let map = if let Some(ref mut source_map) = source_map {
185-
let mut vlq_output: Vec<u8> = Vec::new();
186-
source_map
187-
.write_vlq(&mut vlq_output)
188-
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Error writing sourcemap vlq"))?;
187+
let targets = if !cli_args.targets.is_empty() {
188+
Browsers::from_browserslist(&cli_args.targets).unwrap()
189+
} else if cli_args.browserslist {
190+
Browsers::load_browserslist().unwrap()
191+
} else {
192+
None
193+
};
189194

190-
let sm = SourceMapJson {
191-
version: 3,
192-
mappings: unsafe { String::from_utf8_unchecked(vlq_output) },
193-
sources: source_map.get_sources(),
194-
sources_content: source_map.get_sources_content(),
195-
names: source_map.get_names(),
195+
stylesheet
196+
.minify(MinifyOptions {
197+
targets,
198+
..MinifyOptions::default()
199+
})
200+
.unwrap();
201+
202+
stylesheet
203+
.to_css(PrinterOptions {
204+
minify: cli_args.minify,
205+
source_map: source_map.as_mut(),
206+
project_root: Some(&project_root.to_string_lossy()),
207+
targets,
208+
..PrinterOptions::default()
209+
})
210+
.unwrap()
196211
};
197212

198-
serde_json::to_vec(&sm).ok()
199-
} else {
200-
None
201-
};
213+
let map = if let Some(ref mut source_map) = source_map {
214+
let mut vlq_output: Vec<u8> = Vec::new();
215+
source_map
216+
.write_vlq(&mut vlq_output)
217+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Error writing sourcemap vlq"))?;
202218

203-
if let Some(warnings) = warnings {
204-
let warnings = Arc::try_unwrap(warnings).unwrap().into_inner().unwrap();
205-
for warning in warnings {
206-
eprintln!("{}", warning);
207-
}
208-
}
219+
let sm = SourceMapJson {
220+
version: 3,
221+
mappings: unsafe { String::from_utf8_unchecked(vlq_output) },
222+
sources: source_map.get_sources(),
223+
sources_content: source_map.get_sources_content(),
224+
names: source_map.get_names(),
225+
};
226+
227+
serde_json::to_vec(&sm).ok()
228+
} else {
229+
None
230+
};
209231

210-
if let Some(output_file) = &cli_args.output_file {
211-
let mut code = res.code;
212-
if cli_args.sourcemap {
213-
if let Some(map_buf) = map {
214-
let map_filename: String = output_file.to_owned() + ".map";
215-
code += &format!("\n/*# sourceMappingURL={} */\n", map_filename);
216-
fs::write(map_filename, map_buf)?;
232+
if let Some(warnings) = warnings {
233+
let warnings = Arc::try_unwrap(warnings).unwrap().into_inner().unwrap();
234+
for warning in warnings {
235+
eprintln!("{}", warning);
217236
}
218237
}
219238

220-
let output_path = Path::new(output_file);
221-
if let Some(p) = output_path.parent() {
222-
fs::create_dir_all(p)?
223-
};
224-
fs::write(output_file, code.as_bytes())?;
239+
if let Some(output_file) = &output_file {
240+
let mut code = res.code;
241+
if cli_args.sourcemap {
242+
if let Some(map_buf) = map {
243+
let map_filename = output_file.to_string_lossy() + ".map";
244+
code += &format!("\n/*# sourceMappingURL={} */\n", map_filename);
245+
fs::write(map_filename.as_ref(), map_buf)?;
246+
}
247+
}
225248

226-
if let Some(css_modules) = cli_args.css_modules {
227-
let css_modules_filename = if let Some(name) = css_modules {
228-
name
229-
} else {
230-
infer_css_modules_filename(&output_file)?
249+
if let Some(p) = output_file.parent() {
250+
fs::create_dir_all(p)?
231251
};
232-
if let Some(exports) = res.exports {
233-
let css_modules_json = serde_json::to_string(&exports)?;
234-
fs::write(css_modules_filename, css_modules_json)?;
252+
fs::write(output_file, code.as_bytes())?;
253+
254+
if let Some(css_modules) = &cli_args.css_modules {
255+
let css_modules_filename = if let Some(name) = css_modules {
256+
Cow::Borrowed(name)
257+
} else {
258+
Cow::Owned(infer_css_modules_filename(output_file.as_ref())?)
259+
};
260+
if let Some(exports) = res.exports {
261+
let css_modules_json = serde_json::to_string(&exports)?;
262+
fs::write(css_modules_filename.as_ref(), css_modules_json)?;
263+
}
235264
}
236-
}
237-
} else {
238-
if let Some(exports) = res.exports {
239-
println!(
240-
"{}",
241-
serde_json::json!({
242-
"code": res.code,
243-
"exports": exports
244-
})
245-
);
246265
} else {
247-
println!("{}", res.code);
266+
if let Some(exports) = res.exports {
267+
println!(
268+
"{}",
269+
serde_json::json!({
270+
"code": res.code,
271+
"exports": exports
272+
})
273+
);
274+
} else {
275+
println!("{}", res.code);
276+
}
248277
}
249278
}
250279

251280
Ok(())
252281
}
253282

254-
fn infer_css_modules_filename(output_file: &str) -> Result<String, std::io::Error> {
255-
let path = path::Path::new(output_file);
283+
fn infer_css_modules_filename(path: &Path) -> Result<String, std::io::Error> {
256284
if path.extension() == Some(ffi::OsStr::new("json")) {
257285
Err(io::Error::new(
258286
io::ErrorKind::Other,

0 commit comments

Comments
 (0)