1use std::{
2 fs::{self, File},
3 io::{self, IsTerminal, Read, Write},
4 path::{Component, Path, PathBuf},
5 sync::Arc,
6};
7
8use anyhow::Context;
9use clap::Parser;
10use glob::glob;
11use par_iter::prelude::*;
12use path_absolutize::Absolutize;
13use relative_path::RelativePath;
14use swc_core::{
15 base::{
16 config::{Config, ConfigFile, Options, PluginConfig, SourceMapsConfig},
17 try_with_handler, Compiler, HandlerOpts, TransformOutput,
18 },
19 common::{
20 errors::ColorConfig, sync::Lazy, FileName, FilePathMapping, SourceFile, SourceMap, GLOBALS,
21 },
22 trace_macro::swc_trace,
23};
24use walkdir::WalkDir;
25
26use crate::util::trace::init_trace;
27
28#[derive(Parser)]
30pub struct CompileOptions {
31 #[clap(long = "config-json", value_parser = parse_config)]
36 config: Option<Config>,
37
38 #[clap(long)]
40 config_file: Option<PathBuf>,
41
42 #[clap(long, short = 'f', group = "input")]
45 filename: Option<PathBuf>,
46
47 #[clap(long)]
50 env_name: Option<String>,
51
52 #[clap(long)]
54 ignore: Option<String>,
55
56 #[clap(long)]
58 source_maps: Option<String>,
59
60 #[clap(long)]
62 source_map_target: Option<String>,
63
64 #[clap(long)]
66 source_file_name: Option<String>,
67
68 #[clap(long)]
70 source_root: Option<String>,
71
72 #[clap(long)]
74 watch: bool,
75
76 #[clap(long, group = "output")]
78 out_file: Option<PathBuf>,
79
80 #[clap(long, group = "output")]
82 out_dir: Option<PathBuf>,
83
84 #[clap(long)]
86 extensions: Option<Vec<String>>,
87
88 #[clap(group = "input")]
90 files: Vec<PathBuf>,
91
92 #[clap(long, default_value_t= String::from("js"))]
94 out_file_extension: String,
95
96 #[clap(group = "experimental_trace", long)]
99 experimental_trace: bool,
100
101 #[clap(group = "experimental_trace", long)]
104 trace_out_file: Option<String>,
105 }
112
113fn parse_config(s: &str) -> Result<Config, serde_json::Error> {
114 serde_json::from_str(s)
115}
116
117static COMPILER: Lazy<Arc<Compiler>> = Lazy::new(|| {
118 let cm = Arc::new(SourceMap::new(FilePathMapping::empty()));
119
120 Arc::new(Compiler::new(cm))
121});
122
123static DEFAULT_EXTENSIONS: &[&str] = &["js", "jsx", "es6", "es", "mjs", "ts", "tsx", "cts", "mts"];
125
126#[tracing::instrument(level = "info", skip_all)]
130fn get_files_list(
131 raw_files_input: &[PathBuf],
132 extensions: &[String],
133 ignore_pattern: Option<&str>,
134 _include_dotfiles: bool,
135) -> anyhow::Result<Vec<PathBuf>> {
136 let input_dir = raw_files_input.iter().find(|p| p.is_dir());
137
138 let files = if let Some(input_dir) = input_dir {
139 if raw_files_input.len() > 1 {
140 return Err(anyhow::anyhow!(
141 "Cannot specify multiple files when using a directory as input"
142 ));
143 }
144
145 WalkDir::new(input_dir)
146 .into_iter()
147 .filter_map(|e| e.ok())
148 .map(|e| e.into_path())
149 .filter(|e| e.is_file())
150 .filter(|e| {
151 extensions
152 .iter()
153 .any(|ext| e.extension().map(|v| v == &**ext).unwrap_or(false))
154 })
155 .collect()
156 } else {
157 raw_files_input.to_owned()
158 };
159
160 if let Some(ignore_pattern) = ignore_pattern {
161 let pattern: Vec<PathBuf> = glob(ignore_pattern)?.filter_map(|p| p.ok()).collect();
162
163 return Ok(files
164 .into_iter()
165 .filter(|file_path| !pattern.iter().any(|p| p.eq(file_path)))
166 .collect());
167 }
168
169 Ok(files)
170}
171
172fn resolve_output_file_path(
176 out_dir: &Path,
177 file_path: &Path,
178 file_extension: PathBuf,
179) -> anyhow::Result<PathBuf> {
180 let default = PathBuf::from(".");
181 let base = file_path.parent().unwrap_or(&default).display().to_string();
182
183 let dist_absolute_path = out_dir.absolutize()?;
184
185 let base = RelativePath::new(&*base);
203 let output_path = base.to_logical_path(dist_absolute_path).join(
204 file_path
206 .with_extension(file_extension)
207 .file_name()
208 .expect("Filename should be available"),
209 );
210
211 Ok(output_path)
212}
213
214fn emit_output(
215 mut output: TransformOutput,
216 out_dir: &Option<PathBuf>,
217 file_path: &Path,
218 file_extension: PathBuf,
219) -> anyhow::Result<()> {
220 if let Some(out_dir) = out_dir {
221 let output_file_path = resolve_output_file_path(out_dir, file_path, file_extension)?;
222 let output_dir = output_file_path
223 .parent()
224 .expect("Parent should be available");
225
226 if !output_dir.is_dir() {
227 fs::create_dir_all(output_dir)?;
228 }
229
230 if let Some(ref source_map) = output.map {
231 let source_map_path = output_file_path.with_extension("js.map");
232
233 output.code.push_str("\n//# sourceMappingURL=");
234 output
235 .code
236 .push_str(&source_map_path.file_name().unwrap().to_string_lossy());
237
238 fs::write(source_map_path, source_map)?;
239 }
240
241 fs::write(&output_file_path, &output.code)?;
242
243 if let Some(extra) = &output.output {
244 let mut extra: serde_json::Map<String, serde_json::Value> =
245 serde_json::from_str(extra).context("failed to parse extra output")?;
246
247 if let Some(dts_code) = extra.remove("__swc_isolated_declarations__") {
248 let dts_file_path = output_file_path.with_extension("d.ts");
249 fs::write(dts_file_path, dts_code.as_str().unwrap())?;
250 }
251 }
252 } else {
253 let source_map = if let Some(ref source_map) = output.map {
254 &**source_map
255 } else {
256 ""
257 };
258
259 println!("{}\n{}\n{}", file_path.display(), output.code, source_map,);
260 };
261 Ok(())
262}
263
264fn collect_stdin_input() -> Option<String> {
265 let stdin = io::stdin();
266 if stdin.is_terminal() {
267 return None;
268 }
269
270 let mut buffer = String::new();
271 let result = stdin.lock().read_to_string(&mut buffer);
272
273 if result.is_ok() && !buffer.is_empty() {
274 Some(buffer)
275 } else {
276 None
277 }
278}
279
280struct InputContext {
281 options: Options,
282 fm: Arc<SourceFile>,
283 compiler: Arc<Compiler>,
284 file_path: PathBuf,
285 file_extension: PathBuf,
286}
287
288#[swc_trace]
289impl CompileOptions {
290 fn build_transform_options(&self, file_path: &Option<&Path>) -> anyhow::Result<Options> {
291 let config_file = self.config_file.as_ref().map(|config_file_path| {
292 ConfigFile::Str(config_file_path.to_string_lossy().to_string())
293 });
294
295 let mut options = Options {
296 config: self.config.to_owned().unwrap_or_default(),
297 config_file,
298 ..Options::default()
299 };
300
301 options.config.jsc.experimental.plugins =
302 options.config.jsc.experimental.plugins.map(|plugins| {
303 plugins
304 .into_iter()
305 .map(|p| {
306 let path = Path::new(&p.0);
309 PluginConfig(
310 match path.components().next() {
311 Some(Component::CurDir) | Some(Component::ParentDir) => {
312 path.absolutize().unwrap().display().to_string()
313 }
314 _ => p.0,
315 },
316 p.1,
317 )
318 })
319 .collect()
320 });
321
322 if let Some(file_path) = *file_path {
323 file_path
324 .to_str()
325 .unwrap_or_default()
326 .clone_into(&mut options.filename);
327 }
328
329 if let Some(env_name) = &self.env_name {
330 options.env_name = env_name.to_string();
331 }
332
333 if let Some(source_maps) = &self.source_maps {
334 options.source_maps = Some(match source_maps.as_str() {
335 "false" => SourceMapsConfig::Bool(false),
336 "true" => SourceMapsConfig::Bool(true),
337 value => SourceMapsConfig::Str(value.to_string()),
338 });
339
340 self.source_file_name
341 .clone_into(&mut options.source_file_name);
342 self.source_root.clone_into(&mut options.source_root);
343 }
344
345 Ok(options)
346 }
347
348 fn collect_inputs(&self) -> anyhow::Result<Vec<InputContext>> {
351 let compiler = COMPILER.clone();
352
353 if !self.files.is_empty() {
354 let included_extensions = if let Some(extensions) = &self.extensions {
355 extensions.clone()
356 } else {
357 DEFAULT_EXTENSIONS.iter().map(|v| v.to_string()).collect()
358 };
359
360 return get_files_list(
361 &self.files,
362 &included_extensions,
363 self.ignore.as_deref(),
364 false,
365 )?
366 .iter()
367 .map(|file_path| {
368 self.build_transform_options(&Some(file_path))
369 .and_then(|options| {
370 let fm = compiler
371 .cm
372 .load_file(file_path)
373 .context(format!("Failed to open file {}", file_path.display()));
374 fm.map(|fm| InputContext {
375 options,
376 fm,
377 compiler: compiler.clone(),
378 file_path: file_path.to_path_buf(),
379 file_extension: self.out_file_extension.clone().into(),
380 })
381 })
382 })
383 .collect::<anyhow::Result<Vec<InputContext>>>();
384 }
385
386 let stdin_input = collect_stdin_input();
387 if stdin_input.is_some() && !self.files.is_empty() {
388 anyhow::bail!("Cannot specify inputs from stdin and files at the same time");
389 }
390
391 if let Some(stdin_input) = stdin_input {
392 let options = self.build_transform_options(&self.filename.as_deref())?;
393
394 let fm = compiler.cm.new_source_file(
395 if options.filename.is_empty() {
396 FileName::Anon.into()
397 } else {
398 FileName::Real(options.filename.clone().into()).into()
399 },
400 stdin_input,
401 );
402
403 return Ok(vec![InputContext {
404 options,
405 fm,
406 compiler,
407 file_path: self
408 .filename
409 .clone()
410 .unwrap_or_else(|| PathBuf::from("unknown")),
411 file_extension: self.out_file_extension.clone().into(),
412 }]);
413 }
414
415 anyhow::bail!("Input is empty");
416 }
417
418 fn execute_inner(&self) -> anyhow::Result<()> {
419 let inputs = self.collect_inputs()?;
420
421 let execute = |compiler: Arc<Compiler>, fm: Arc<SourceFile>, options: Options| {
422 let color = ColorConfig::Always;
423 let skip_filename = false;
424
425 try_with_handler(
426 compiler.cm.clone(),
427 HandlerOpts {
428 color,
429 skip_filename,
430 },
431 |handler| {
432 GLOBALS.set(&Default::default(), || {
433 compiler.process_js_file(fm, handler, &options)
434 })
435 },
436 )
437 .map_err(|e| e.to_pretty_error())
438 };
439
440 if let Some(single_out_file) = self.out_file.as_ref() {
441 let result: anyhow::Result<Vec<TransformOutput>> = inputs
442 .into_par_iter()
443 .map(
444 |InputContext {
445 compiler,
446 fm,
447 options,
448 ..
449 }| execute(compiler, fm, options),
450 )
451 .collect();
452
453 fs::create_dir_all(
454 single_out_file
455 .parent()
456 .expect("Parent should be available"),
457 )?;
458 let mut buf = File::create(single_out_file)?;
459 let mut buf_srcmap = None;
460 let mut buf_dts = None;
461 let mut source_map_path = None;
462
463 for r in result?.iter() {
465 if let Some(src_map) = r.map.as_ref() {
466 if buf_srcmap.is_none() {
467 let map_out_file = if let Some(source_map_target) = &self.source_map_target
468 {
469 source_map_path = Some(source_map_target.clone());
470 source_map_target.into()
471 } else {
472 let map_out_file = single_out_file.with_extension(format!(
473 "{}map",
474 if let Some(ext) = single_out_file.extension() {
475 format!("{}.", ext.to_string_lossy())
476 } else {
477 "".to_string()
478 }
479 ));
480
481 source_map_path = Some(
484 map_out_file
485 .file_name()
486 .unwrap()
487 .to_string_lossy()
488 .to_string(),
489 );
490 map_out_file
491 };
492 buf_srcmap = Some(File::create(map_out_file)?);
493 }
494
495 buf_srcmap
496 .as_ref()
497 .expect("Srcmap buffer should be available")
498 .write(src_map.as_bytes())
499 .and(Ok(()))?;
500 }
501
502 if let Some(extra) = &r.output {
503 let mut extra: serde_json::Map<String, serde_json::Value> =
504 serde_json::from_str(extra).context("failed to parse extra output")?;
505
506 if let Some(dts_code) = extra.remove("__swc_isolated_declarations__") {
507 if buf_dts.is_none() {
508 let dts_file_path = single_out_file.with_extension("d.ts");
509 buf_dts = Some(File::create(dts_file_path)?);
510 }
511
512 let dts_code = dts_code.as_str().expect("dts code should be string");
513 buf_dts
514 .as_ref()
515 .expect("dts buffer should be available")
516 .write(dts_code.as_bytes())
517 .and(Ok(()))?;
518 }
519 }
520
521 buf.write(r.code.as_bytes()).and(Ok(()))?;
522 }
523
524 if let Some(source_map_path) = source_map_path {
525 buf.write_all(b"\n//# sourceMappingURL=")?;
526 buf.write_all(source_map_path.as_bytes())?;
527 }
528
529 buf.flush()
530 .context("Failed to write output into single file")
531 } else {
532 inputs.into_par_iter().try_for_each(
533 |InputContext {
534 compiler,
535 fm,
536 options,
537 file_path,
538 file_extension,
539 }| {
540 let result = execute(compiler, fm, options);
541
542 match result {
543 Ok(output) => {
544 emit_output(output, &self.out_dir, &file_path, file_extension)
545 }
546 Err(e) => Err(e),
547 }
548 },
549 )
550 }
551 }
552}
553
554#[swc_trace]
555impl super::CommandRunner for CompileOptions {
556 fn execute(&self) -> anyhow::Result<()> {
557 let guard = if self.experimental_trace {
558 init_trace(&self.trace_out_file)
559 } else {
560 None
561 };
562
563 let ret = self.execute_inner();
564
565 if let Some(guard) = guard {
566 guard.flush();
567 drop(guard);
568 }
569
570 ret
571 }
572}