feat(cli): Support stdin (#3688)

This commit is contained in:
OJ Kwon 2022-02-22 20:38:43 -08:00 committed by GitHub
parent 61d9e329c9
commit 3cf3b20f15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 150 additions and 36 deletions

1
Cargo.lock generated
View File

@ -2740,6 +2740,7 @@ name = "swc_cli"
version = "0.8.0"
dependencies = [
"anyhow",
"atty",
"clap",
"path-absolutize",
"rayon",

View File

@ -22,6 +22,7 @@ swc_common = { version = "0.17.5", path = "../swc_common" }
relative-path = "1.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["unbounded_depth"] }
atty = "0.2.14"
[dependencies.path-absolutize]
version = "3.0.11"

View File

@ -1,5 +1,6 @@
use std::{
fs,
io::{self, BufRead},
path::{Path, PathBuf},
sync::Arc,
};
@ -13,26 +14,75 @@ use swc::{
config::{Config, Options},
try_with_handler, Compiler, TransformOutput,
};
use swc_common::{sync::Lazy, FilePathMapping, SourceMap};
use swc_common::{sync::Lazy, FileName, FilePathMapping, SourceMap};
use walkdir::WalkDir;
/// Configuration option for transform files.
#[derive(Parser)]
pub struct CompileOptions {
/// Override a config from .swcrc file.
#[clap(long)]
config: Option<Vec<String>>,
/// Path to a .swcrc file to use
#[clap(long = "config")]
#[clap(long)]
config_file: Option<PathBuf>,
/// Files to compile
files: Vec<PathBuf>,
/// Filename to use when reading from stdin - this will be used in
/// source-maps, errors etc
#[clap(long, short = 'f', group = "input")]
filename: Option<PathBuf>,
/// The name of the 'env' to use when loading configs and plugins. Defaults
/// to the value of SWC_ENV, or else NODE_ENV, or else development.
#[clap(long)]
env_name: Option<String>,
/// List of glob paths to not compile.
#[clap(long)]
ignore: Option<String>,
/// Values: true|false|inline|both
#[clap(long)]
source_maps: Option<String>,
/// Define the file for the source map.
#[clap(long)]
source_maps_target: Option<String>,
/// Set sources[0] on returned source map
#[clap(long)]
source_file_name: Option<String>,
/// The root from which all sources are relative.
#[clap(long)]
source_root: Option<String>,
/// Automatically recompile files on change
#[clap(long)]
watch: bool,
/// Compile all input files into a single file.
#[clap(long, group = "output")]
out_file: Option<PathBuf>,
/// The output directory
#[clap(long)]
#[clap(long, group = "output")]
out_dir: Option<PathBuf>,
/// Specify specific file extensions to compile.
#[clap(long)]
extensions: Option<Vec<String>>,
/// Files to compile
#[clap(group = "input")]
files: Vec<PathBuf>,
//Flags legacy @swc/cli supports, might need some thoughts if we need support same.
//log_watch_compilation: bool,
//copy_files: bool,
//include_dotfiles: bool,
//only: Option<String>,
//no_swcrc: bool,
}
static COMPILER: Lazy<Arc<Compiler>> = Lazy::new(|| {
@ -80,7 +130,7 @@ fn get_files_list(
fn build_transform_options(
config_file: &Option<PathBuf>,
file_path: &Path,
file_path: &Option<&Path>,
) -> anyhow::Result<Options> {
let base_options = Options::default();
let base_config = Config::default();
@ -94,12 +144,17 @@ fn build_transform_options(
None
};
Ok(Options {
let mut ret = Options {
config: Config { ..base_config },
config_file,
filename: file_path.to_str().unwrap_or_default().to_owned(),
..base_options
})
};
if let Some(file_path) = file_path {
ret.filename = file_path.to_str().unwrap_or_default().to_owned();
}
Ok(ret)
}
/// Calculate full, absolute path to the file to emit.
@ -176,39 +231,96 @@ fn emit_output(
Ok(())
}
fn collect_stdin_input() -> Option<String> {
if atty::is(atty::Stream::Stdin) {
return None;
}
Some(
io::stdin()
.lock()
.lines()
.map(|line| line.expect("Not able to read stdin"))
.collect::<Vec<String>>()
.join("\n"),
)
}
impl super::CommandRunner for CompileOptions {
fn execute(&self) -> anyhow::Result<()> {
let included_extensions = if let Some(extensions) = &self.extensions {
extensions.clone()
} else {
DEFAULT_EXTENSIONS.iter().map(|v| v.to_string()).collect()
};
let stdin_input = collect_stdin_input();
let files = get_files_list(&self.files, &included_extensions, false)?;
let cm = COMPILER.clone();
if let Some(out_dir) = &self.out_dir {
fs::create_dir_all(out_dir)?;
if stdin_input.is_some() && !self.files.is_empty() {
anyhow::bail!("Cannot specify inputs from stdin and files at the same time");
}
files
.into_par_iter()
.try_for_each_with(cm, |compiler, file_path| {
let result = try_with_handler(compiler.cm.clone(), false, |handler| {
let options = build_transform_options(&self.config_file, &file_path)?;
let fm = compiler
.cm
.load_file(&file_path)
.context("failed to load file")?;
compiler.process_js_file(fm, handler, &options)
if stdin_input.is_none() && self.files.is_empty() {
anyhow::bail!("Input is empty");
}
if let Some(stdin_input) = stdin_input {
let comp = COMPILER.clone();
let result = try_with_handler(comp.cm.clone(), false, |handler| {
let options =
build_transform_options(&self.config_file, &self.filename.as_deref())?;
let fm = comp.cm.new_source_file(
if options.filename.is_empty() {
FileName::Anon
} else {
FileName::Real(options.filename.clone().into())
},
stdin_input,
);
comp.process_js_file(fm, handler, &options)
});
match result {
Ok(output) => emit_output(
&output,
&self.out_dir,
self.filename.as_ref().unwrap_or(&PathBuf::from("unknown")),
)?,
Err(e) => return Err(e),
};
return Ok(());
}
if !self.files.is_empty() {
let included_extensions = if let Some(extensions) = &self.extensions {
extensions.clone()
} else {
DEFAULT_EXTENSIONS.iter().map(|v| v.to_string()).collect()
};
let files = get_files_list(&self.files, &included_extensions, false)?;
let cm = COMPILER.clone();
return files
.into_par_iter()
.try_for_each_with(cm, |compiler, file_path| {
let result = try_with_handler(compiler.cm.clone(), false, |handler| {
let options =
build_transform_options(&self.config_file, &Some(&file_path))?;
let fm = compiler
.cm
.load_file(&file_path)
.context("failed to load file")?;
compiler.process_js_file(fm, handler, &options)
});
match result {
Ok(output) => emit_output(&output, &self.out_dir, &file_path)?,
Err(e) => return Err(e),
};
Ok(())
});
}
match result {
Ok(output) => emit_output(&output, &self.out_dir, &file_path)?,
Err(e) => return Err(e),
};
Ok(())
})
Ok(())
}
}