use std::{
fs::{self, File},
io::{self, IsTerminal, Read, Write},
path::{Component, Path, PathBuf},
sync::Arc,
};
use anyhow::Context;
use clap::Parser;
use glob::glob;
use par_iter::prelude::*;
use path_absolutize::Absolutize;
use relative_path::RelativePath;
use swc_core::{
base::{
config::{
default_swcrc, Config, ConfigFile, Options, PluginConfig, RootMode, SourceMapsConfig,
},
try_with_handler, Compiler, HandlerOpts, TransformOutput,
},
common::{
errors::ColorConfig, sync::Lazy, FileName, FilePathMapping, SourceFile, SourceMap, GLOBALS,
},
trace_macro::swc_trace,
};
use walkdir::WalkDir;
use crate::util::trace::init_trace;
#[derive(Parser)]
pub struct CompileOptions {
#[clap(long = "config-json", value_parser = parse_config)]
config: Option<Config>,
#[clap(long)]
config_file: Option<PathBuf>,
#[clap(long, value_parser = parse_root_mode)]
root_mode: Option<RootMode>,
#[clap(long, short = 'f', group = "input")]
filename: Option<PathBuf>,
#[clap(long)]
env_name: Option<String>,
#[clap(long)]
ignore: Option<String>,
#[clap(long)]
source_maps: Option<String>,
#[clap(long)]
source_map_target: Option<String>,
#[clap(long)]
source_file_name: Option<String>,
#[clap(long)]
source_root: Option<String>,
#[clap(long)]
watch: bool,
#[clap(long, group = "output")]
out_file: Option<PathBuf>,
#[clap(long, group = "output")]
out_dir: Option<PathBuf>,
#[clap(long)]
extensions: Option<Vec<String>>,
#[clap(group = "input")]
files: Vec<PathBuf>,
#[clap(long, default_value_t= String::from("js"))]
out_file_extension: String,
#[clap(group = "experimental_trace", long)]
experimental_trace: bool,
#[clap(group = "experimental_trace", long)]
trace_out_file: Option<String>,
}
fn parse_config(s: &str) -> Result<Config, serde_json::Error> {
serde_json::from_str(s)
}
fn parse_root_mode(s: &str) -> Result<RootMode, String> {
match s {
"root" => Ok(RootMode::Root),
"upward" => Ok(RootMode::Upward),
"upward-optional" => Ok(RootMode::UpwardOptional),
_ => Err(format!(
"Invalid root mode '{s}'. Valid values are: root, upward, upward-optional"
)),
}
}
static COMPILER: Lazy<Arc<Compiler>> = Lazy::new(|| {
let cm = Arc::new(SourceMap::new(FilePathMapping::empty()));
Arc::new(Compiler::new(cm))
});
static DEFAULT_EXTENSIONS: &[&str] = &["js", "jsx", "es6", "es", "mjs", "ts", "tsx", "cts", "mts"];
#[tracing::instrument(level = "info", skip_all)]
fn get_files_list(
raw_files_input: &[PathBuf],
extensions: &[String],
ignore_pattern: Option<&str>,
_include_dotfiles: bool,
) -> anyhow::Result<Vec<PathBuf>> {
let input_dir = raw_files_input.iter().find(|p| p.is_dir());
let files = if let Some(input_dir) = input_dir {
if raw_files_input.len() > 1 {
return Err(anyhow::anyhow!(
"Cannot specify multiple files when using a directory as input"
));
}
WalkDir::new(input_dir)
.into_iter()
.filter_map(|e| e.ok())
.map(|e| e.into_path())
.filter(|e| e.is_file())
.filter(|e| {
extensions
.iter()
.any(|ext| e.extension().map(|v| v == &**ext).unwrap_or(false))
})
.collect()
} else {
raw_files_input.to_owned()
};
if let Some(ignore_pattern) = ignore_pattern {
let pattern: Vec<PathBuf> = glob(ignore_pattern)?.filter_map(|p| p.ok()).collect();
return Ok(files
.into_iter()
.filter(|file_path| !pattern.iter().any(|p| p.eq(file_path)))
.collect());
}
Ok(files)
}
fn resolve_output_file_path(
out_dir: &Path,
file_path: &Path,
file_extension: PathBuf,
) -> anyhow::Result<PathBuf> {
let default = PathBuf::from(".");
let base = file_path.parent().unwrap_or(&default).display().to_string();
let dist_absolute_path = out_dir.absolutize()?;
let base = RelativePath::new(&*base);
let output_path = base.to_logical_path(dist_absolute_path).join(
file_path
.with_extension(file_extension)
.file_name()
.expect("Filename should be available"),
);
Ok(output_path)
}
fn emit_output(
mut output: TransformOutput,
out_dir: &Option<PathBuf>,
file_path: &Path,
file_extension: PathBuf,
) -> anyhow::Result<()> {
if let Some(out_dir) = out_dir {
let output_file_path = resolve_output_file_path(out_dir, file_path, file_extension)?;
let output_dir = output_file_path
.parent()
.expect("Parent should be available");
if !output_dir.is_dir() {
fs::create_dir_all(output_dir)?;
}
if let Some(ref source_map) = output.map {
let source_map_path = output_file_path.with_extension("js.map");
output.code.push_str("\n//# sourceMappingURL=");
output
.code
.push_str(&source_map_path.file_name().unwrap().to_string_lossy());
fs::write(source_map_path, source_map)?;
}
fs::write(&output_file_path, &output.code)?;
if let Some(extra) = &output.output {
let mut extra: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(extra).context("failed to parse extra output")?;
if let Some(dts_code) = extra.remove("__swc_isolated_declarations__") {
let dts_file_path = output_file_path.with_extension("d.ts");
fs::write(dts_file_path, dts_code.as_str().unwrap())?;
}
}
} else {
let source_map = if let Some(ref source_map) = output.map {
&**source_map
} else {
""
};
eprintln!("{}", file_path.display());
println!("{}\n{}", output.code, source_map,);
};
Ok(())
}
fn collect_stdin_input() -> Option<String> {
let stdin = io::stdin();
if stdin.is_terminal() {
return None;
}
let mut buffer = String::new();
let result = stdin.lock().read_to_string(&mut buffer);
if result.is_ok() && !buffer.is_empty() {
Some(buffer)
} else {
None
}
}
struct InputContext {
options: Options,
fm: Arc<SourceFile>,
compiler: Arc<Compiler>,
file_path: PathBuf,
file_extension: PathBuf,
}
#[swc_trace]
impl CompileOptions {
fn build_transform_options(&self, file_path: &Option<&Path>) -> anyhow::Result<Options> {
let config_file = self.config_file.as_ref().map(|config_file_path| {
ConfigFile::Str(config_file_path.to_string_lossy().to_string())
});
let mut options = Options {
config: self.config.to_owned().unwrap_or_default(),
config_file,
swcrc: default_swcrc(),
..Options::default()
};
options.config.jsc.experimental.plugins =
options.config.jsc.experimental.plugins.map(|plugins| {
plugins
.into_iter()
.map(|p| {
let path = Path::new(&p.0);
PluginConfig(
match path.components().next() {
Some(Component::CurDir) | Some(Component::ParentDir) => {
path.absolutize().unwrap().display().to_string()
}
_ => p.0,
},
p.1,
)
})
.collect()
});
if let Some(file_path) = *file_path {
file_path
.to_str()
.unwrap_or_default()
.clone_into(&mut options.filename);
}
if let Some(env_name) = &self.env_name {
options.env_name = env_name.to_string();
}
if let Some(root_mode) = self.root_mode {
options.root_mode = root_mode;
}
if let Some(source_maps) = &self.source_maps {
options.source_maps = Some(match source_maps.as_str() {
"false" => SourceMapsConfig::Bool(false),
"true" => SourceMapsConfig::Bool(true),
value => SourceMapsConfig::Str(value.to_string()),
});
self.source_file_name
.clone_into(&mut options.source_file_name);
self.source_root.clone_into(&mut options.source_root);
}
Ok(options)
}
fn collect_inputs(&self) -> anyhow::Result<Vec<InputContext>> {
let compiler = COMPILER.clone();
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()
};
return get_files_list(
&self.files,
&included_extensions,
self.ignore.as_deref(),
false,
)?
.iter()
.map(|file_path| {
self.build_transform_options(&Some(file_path))
.and_then(|options| {
let fm = compiler
.cm
.load_file(file_path)
.context(format!("Failed to open file {}", file_path.display()));
fm.map(|fm| InputContext {
options,
fm,
compiler: compiler.clone(),
file_path: file_path.to_path_buf(),
file_extension: self.out_file_extension.clone().into(),
})
})
})
.collect::<anyhow::Result<Vec<InputContext>>>();
}
let stdin_input = collect_stdin_input();
if stdin_input.is_some() && !self.files.is_empty() {
anyhow::bail!("Cannot specify inputs from stdin and files at the same time");
}
if let Some(stdin_input) = stdin_input {
let options = self.build_transform_options(&self.filename.as_deref())?;
let fm = compiler.cm.new_source_file(
if options.filename.is_empty() {
FileName::Anon.into()
} else {
FileName::Real(options.filename.clone().into()).into()
},
stdin_input,
);
return Ok(vec![InputContext {
options,
fm,
compiler,
file_path: self
.filename
.clone()
.unwrap_or_else(|| PathBuf::from("unknown")),
file_extension: self.out_file_extension.clone().into(),
}]);
}
anyhow::bail!("Input is empty");
}
fn execute_inner(&self) -> anyhow::Result<()> {
let inputs = self.collect_inputs()?;
let execute = |compiler: Arc<Compiler>, fm: Arc<SourceFile>, options: Options| {
let color = ColorConfig::Always;
let skip_filename = false;
try_with_handler(
compiler.cm.clone(),
HandlerOpts {
color,
skip_filename,
},
|handler| {
GLOBALS.set(&Default::default(), || {
compiler.process_js_file(fm, handler, &options)
})
},
)
.map_err(|e| e.to_pretty_error())
};
if let Some(single_out_file) = self.out_file.as_ref() {
let result: anyhow::Result<Vec<TransformOutput>> = inputs
.into_par_iter()
.map(
|InputContext {
compiler,
fm,
options,
..
}| execute(compiler, fm, options),
)
.collect();
fs::create_dir_all(
single_out_file
.parent()
.expect("Parent should be available"),
)?;
let mut buf = File::create(single_out_file)?;
let mut buf_srcmap = None;
let mut buf_dts = None;
let mut source_map_path = None;
for r in result?.iter() {
if let Some(src_map) = r.map.as_ref() {
if buf_srcmap.is_none() {
let map_out_file = if let Some(source_map_target) = &self.source_map_target
{
source_map_path = Some(source_map_target.clone());
source_map_target.into()
} else {
let map_out_file = single_out_file.with_extension(format!(
"{}map",
if let Some(ext) = single_out_file.extension() {
format!("{}.", ext.to_string_lossy())
} else {
"".to_string()
}
));
source_map_path = Some(
map_out_file
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
);
map_out_file
};
buf_srcmap = Some(File::create(map_out_file)?);
}
buf_srcmap
.as_ref()
.expect("Srcmap buffer should be available")
.write(src_map.as_bytes())
.and(Ok(()))?;
}
if let Some(extra) = &r.output {
let mut extra: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(extra).context("failed to parse extra output")?;
if let Some(dts_code) = extra.remove("__swc_isolated_declarations__") {
if buf_dts.is_none() {
let dts_file_path = single_out_file.with_extension("d.ts");
buf_dts = Some(File::create(dts_file_path)?);
}
let dts_code = dts_code.as_str().expect("dts code should be string");
buf_dts
.as_ref()
.expect("dts buffer should be available")
.write(dts_code.as_bytes())
.and(Ok(()))?;
}
}
buf.write(r.code.as_bytes()).and(Ok(()))?;
}
if let Some(source_map_path) = source_map_path {
buf.write_all(b"\n//# sourceMappingURL=")?;
buf.write_all(source_map_path.as_bytes())?;
}
buf.flush()
.context("Failed to write output into single file")
} else {
inputs.into_par_iter().try_for_each(
|InputContext {
compiler,
fm,
options,
file_path,
file_extension,
}| {
let result = execute(compiler, fm, options);
match result {
Ok(output) => {
emit_output(output, &self.out_dir, &file_path, file_extension)
}
Err(e) => Err(e),
}
},
)
}
}
}
#[swc_trace]
impl super::CommandRunner for CompileOptions {
fn execute(&self) -> anyhow::Result<()> {
let guard = if self.experimental_trace {
init_trace(&self.trace_out_file)
} else {
None
};
let ret = self.execute_inner();
if let Some(guard) = guard {
guard.flush();
drop(guard);
}
ret
}
}