swc_cli_impl/commands/
compile.rs

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/// Configuration option for transform files.
29#[derive(Parser)]
30pub struct CompileOptions {
31    /// Experimental: provide an additional JSON config object to override the
32    /// .swcrc. Can be used to provide experimental plugin configuration,
33    /// including plugin imports that are explicitly relative, starting with `.`
34    /// or `..`
35    #[clap(long = "config-json", value_parser = parse_config)]
36    config: Option<Config>,
37
38    /// Path to a .swcrc file to use
39    #[clap(long)]
40    config_file: Option<PathBuf>,
41
42    /// Filename to use when reading from stdin - this will be used in
43    /// source-maps, errors etc
44    #[clap(long, short = 'f', group = "input")]
45    filename: Option<PathBuf>,
46
47    /// The name of the 'env' to use when loading configs and plugins. Defaults
48    /// to the value of SWC_ENV, or else NODE_ENV, or else development.
49    #[clap(long)]
50    env_name: Option<String>,
51
52    /// List of glob paths to not compile.
53    #[clap(long)]
54    ignore: Option<String>,
55
56    /// Values: true|false|inline|both
57    #[clap(long)]
58    source_maps: Option<String>,
59
60    /// Define the file for the source map.
61    #[clap(long)]
62    source_map_target: Option<String>,
63
64    /// Set sources[0] on returned source map
65    #[clap(long)]
66    source_file_name: Option<String>,
67
68    /// The root from which all sources are relative.
69    #[clap(long)]
70    source_root: Option<String>,
71
72    /// Automatically recompile files on change
73    #[clap(long)]
74    watch: bool,
75
76    /// Compile all input files into a single file.
77    #[clap(long, group = "output")]
78    out_file: Option<PathBuf>,
79
80    /// The output directory
81    #[clap(long, group = "output")]
82    out_dir: Option<PathBuf>,
83
84    /// Specify specific file extensions to compile.
85    #[clap(long)]
86    extensions: Option<Vec<String>>,
87
88    /// Files to compile
89    #[clap(group = "input")]
90    files: Vec<PathBuf>,
91
92    /// Use a specific extension for the output files
93    #[clap(long, default_value_t= String::from("js"))]
94    out_file_extension: String,
95
96    /// Enable experimental trace profiling
97    /// generates trace compatible with trace event format.
98    #[clap(group = "experimental_trace", long)]
99    experimental_trace: bool,
100
101    /// Set file name for the trace output. If not specified,
102    /// `trace-{unix epoch time}.json` will be used by default.
103    #[clap(group = "experimental_trace", long)]
104    trace_out_file: Option<String>,
105    /*Flags legacy @swc/cli supports, might need some thoughts if we need support same.
106     *log_watch_compilation: bool,
107     *copy_files: bool,
108     *include_dotfiles: bool,
109     *only: Option<String>,
110     *no_swcrc: bool, */
111}
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
123/// List of file extensions supported by default.
124static DEFAULT_EXTENSIONS: &[&str] = &["js", "jsx", "es6", "es", "mjs", "ts", "tsx", "cts", "mts"];
125
126/// Infer list of files to be transformed from cli arguments.
127/// If given input is a directory, it'll traverse it and collect all supported
128/// files.
129#[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
172/// Calculate full, absolute path to the file to emit.
173/// Currently this is quite naive calculation based on assumption input file's
174/// path and output dir are relative to the same directory.
175fn 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    // These are possible combinations between input to output dir.
186    // cwd: /c/github/swc
187    //
188    // Input
189    // 1. Relative to cwd                   : ./crates/swc/tests/serde/a.js
190    // 2. Relative to cwd, traverse up      : ../repo/some/dir/b.js
191    // 3. Absolute path, relative to cwd: /c/github/swc/crates/swc/tests/serde/a.js
192    // 4. Absolute path, not relative to cwd: /c/github/repo/some/dir/b.js
193    //
194    // OutDir
195    // a. Relative to cwd: ./dist
196    // b. Relative to cwd, traverse up: ../outer_dist
197    // c. Absolute path: /c/github/swc/dist
198    // d. Absolute path, not relative to cwd: /c/github/outer_dist
199    //
200    // It is unclear how to calculate output path when either input or output is not
201    // relative to cwd (2,4 and b,d) and it is UB for now.
202    let base = RelativePath::new(&*base);
203    let output_path = base.to_logical_path(dist_absolute_path).join(
204        // Custom output file extension is not supported yet
205        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                        // if the path starts with . or .., then turn it into an absolute path using
307                        // the current working directory as the base
308                        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    /// Create canonical list of inputs to be processed across stdin / single
349    /// file / multiple files.
350    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            // write all transformed files to single output buf
464            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                            // Get the filename of the source map, as the source map will
482                            // be created in the same directory next to the output.
483                            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}