swc_cli_impl/commands/
plugin.rs

1use std::{
2    fs::{self, create_dir_all, File, OpenOptions},
3    io::{BufRead, BufReader, ErrorKind, Write},
4    path::{Path, PathBuf},
5};
6
7use anyhow::{Context, Result};
8use clap::{ArgEnum, Parser, Subcommand};
9use swc_core::diagnostics::get_core_engine_diagnostics;
10
11#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, ArgEnum)]
12pub enum PluginTargetType {
13    /// wasm32-unknown-unknown target.
14    Wasm32UnknownUnknown,
15    /// wasm32-wasip1 target.
16    Wasm32Wasip1,
17}
18
19#[derive(Parser, Debug)]
20pub struct PluginScaffoldOptions {
21    /// Set the resulting plugin name, defaults to the directory name
22    #[clap(long)]
23    pub name: Option<String>,
24
25    /// Sets default build target type of the plugin.
26    ///
27    /// "wasm32-wasip1" enables wasi (https://github.com/WebAssembly/WASI) support for the generated
28    /// binary which allows to use macros like 'println!' or 'dbg!' and other
29    /// system-related calls.
30    ///
31    /// "wasm32-unknown-unknown" will makes those calls as no-op, instead
32    /// generates slightly smaller binaries.
33    #[clap(long, arg_enum)]
34    pub target_type: PluginTargetType,
35
36    pub path: PathBuf,
37}
38
39/// Infer package name of the plugin if not specified via cli options.
40fn get_name(option: &PluginScaffoldOptions) -> Result<&str> {
41    if let Some(ref name) = option.name {
42        return Ok(name);
43    }
44
45    let file_name = option.path.file_name().ok_or_else(|| {
46        anyhow::format_err!(
47            "cannot auto-detect package name from path {:?} ; use --name to override",
48            option.path.as_os_str()
49        )
50    })?;
51
52    file_name.to_str().ok_or_else(|| {
53        anyhow::format_err!(
54            "cannot create package with a non-unicode name: {:?}",
55            file_name
56        )
57    })
58}
59
60/// Generate ignore file for the project.
61///
62/// Note: this is slim implementation does not support different vcs other than
63/// git at the moment.
64fn write_ignore_file(base_path: &Path) -> Result<()> {
65    let ignore_list: Vec<String> = ["/target", "^target/", "target"]
66        .iter()
67        .map(|v| v.to_string())
68        .collect();
69
70    let ignore_file_path = base_path.join(".gitignore");
71
72    let ignore: String = match File::open(&ignore_file_path) {
73        Err(err) => match err {
74            io_err if io_err.kind() == ErrorKind::NotFound => ignore_list.join("\n") + "\n",
75            _ => return Err(err).context("failed to open .gitignore"),
76        },
77        Ok(file) => {
78            let existing = BufReader::new(file);
79
80            let existing_items = existing.lines().collect::<Result<Vec<_>, _>>().unwrap();
81            let mut out = String::new();
82
83            out.push_str("\n\n# Added by swc\n");
84            if ignore_list.iter().any(|item| existing_items.contains(item)) {
85                out.push_str("#\n# already existing elements were commented out\n");
86            }
87            out.push('\n');
88
89            for item in &ignore_list {
90                if existing_items.contains(item) {
91                    out.push('#');
92                }
93                out.push_str(item);
94                out.push('\n');
95            }
96
97            out
98        }
99    };
100
101    let mut f = OpenOptions::new()
102        .append(true)
103        .create(true)
104        .open(&ignore_file_path)?;
105
106    write!(f, "{ignore}").context("failed to write to .gitignore file")?;
107
108    Ok(())
109}
110
111impl super::CommandRunner for PluginScaffoldOptions {
112    /// Create a rust project for the plugin from template.
113    /// This largely mimic https://github.com/rust-lang/cargo/blob/master/src/cargo/ops/cargo_new.rs,
114    /// but also thinner implementation based on some assumptions like skipping
115    /// to support non-git based vcs.
116    fn execute(&self) -> Result<()> {
117        let path = &self.path;
118        if path.exists() {
119            anyhow::bail!("destination `{}` already exists", path.display())
120        }
121
122        let name = get_name(self)?;
123
124        // Choose to rely on system's git binary instead of depends on git lib for now
125        // to avoid bring in large / heavy dependencies into cli binaries.
126        // Depends on our usecase grows, we can revisit this.
127        let mut base_git_cmd = if cfg!(target_os = "windows") {
128            let mut c = std::process::Command::new("cmd");
129            c.arg("/C").arg("git");
130            c
131        } else {
132            std::process::Command::new("git")
133        };
134
135        // init git repo
136        base_git_cmd
137            .args(["init", name])
138            .output()
139            .context("failed to create dir for the plugin")?;
140
141        // generate .gitignore
142        write_ignore_file(path)?;
143
144        let core_engine = get_core_engine_diagnostics();
145        let swc_core_version: Vec<&str> = core_engine.package_semver.split('.').collect();
146        // We'll pick semver major.minor, but allow any patch version.
147        let swc_core_version = format!("{}.{}.*", swc_core_version[0], swc_core_version[1]);
148
149        // Create `Cargo.toml` file with necessary sections
150        fs::write(
151            path.join("Cargo.toml"),
152            format!(
153                r#"[package]
154name = "{name}"
155version = "0.1.0"
156edition = "2021"
157
158[lib]
159crate-type = ["cdylib"]
160
161[profile.release]
162lto = true
163
164[dependencies]
165serde = "1"
166swc_core = {{ version = "{swc_core_version}", features = ["ecma_plugin_transform"] }}
167
168# .cargo/config.toml defines few alias to build plugin.
169# cargo build-wasip1 generates wasm32-wasip1 binary
170# cargo build-wasm32 generates wasm32-unknown-unknown binary.
171"#
172            )
173            .as_bytes(),
174        )
175        .context("failed to write Cargo.toml file")?;
176
177        let build_target = match self.target_type {
178            PluginTargetType::Wasm32UnknownUnknown => "wasm32-unknown-unknown",
179            PluginTargetType::Wasm32Wasip1 => "wasm32-wasip1",
180        };
181
182        let build_alias = match self.target_type {
183            PluginTargetType::Wasm32UnknownUnknown => "build-wasm32",
184            PluginTargetType::Wasm32Wasip1 => "build-wasip1",
185        };
186
187        // Create `.cargo/config.toml` file for build target
188        let cargo_config_path = path.join(".cargo");
189        create_dir_all(&cargo_config_path).context("`create_dir_all` failed")?;
190        fs::write(
191            cargo_config_path.join("config.toml"),
192            r#"# These command aliases are not final, may change
193[alias]
194# Alias to build actual plugin binary for the specified target.
195build-wasip1 = "build --target wasm32-wasip1"
196build-wasm32 = "build --target wasm32-unknown-unknown"
197"#
198            .as_bytes(),
199        )
200        .context("failed to write config toml file")?;
201
202        // Create package.json for npm package publishing.
203        let dist_output_path = format!(
204            "target/{}/release/{}.wasm",
205            build_target,
206            name.replace('-', "_")
207        );
208        fs::write(
209            path.join("package.json"),
210            format!(
211                r#"{{
212    "name": "{name}",
213    "version": "0.1.0",
214    "description": "",
215    "author": "",
216    "license": "ISC",
217    "keywords": ["swc-plugin"],
218    "main": "{dist_output_path}",
219    "scripts": {{
220        "prepublishOnly": "cargo {build_alias} --release"
221    }},
222    "files": [],
223    "preferUnplugged": true
224}}
225"#
226            )
227            .as_bytes(),
228        )
229        .context("failed to write package.json file")?;
230
231        // Create entrypoint src file
232        let src_path = path.join("src");
233        create_dir_all(&src_path)?;
234        fs::write(
235            src_path.join("lib.rs"),
236            r##"use swc_core::ecma::{
237    ast::Program,
238    transforms::testing::test_inline,
239    visit::{visit_mut_pass, FoldWith, VisitMut},
240};
241use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};
242
243pub struct TransformVisitor;
244
245impl VisitMut for TransformVisitor {
246    // Implement necessary visit_mut_* methods for actual custom transform.
247    // A comprehensive list of possible visitor methods can be found here:
248    // https://rustdoc.swc.rs/swc_ecma_visit/trait.VisitMut.html
249}
250
251/// An example plugin function with macro support.
252/// `plugin_transform` macro interop pointers into deserialized structs, as well
253/// as returning ptr back to host.
254///
255/// It is possible to opt out from macro by writing transform fn manually
256/// if plugin need to handle low-level ptr directly via
257/// `__transform_plugin_process_impl(
258///     ast_ptr: *const u8, ast_ptr_len: i32,
259///     unresolved_mark: u32, should_enable_comments_proxy: i32) ->
260///     i32 /*  0 for success, fail otherwise.
261///             Note this is only for internal pointer interop result,
262///             not actual transform result */`
263///
264/// This requires manual handling of serialization / deserialization from ptrs.
265/// Refer swc_plugin_macro to see how does it work internally.
266#[plugin_transform]
267pub fn process_transform(program: Program, _metadata: TransformPluginProgramMetadata) -> Program {
268    program.fold_with(&mut visit_mut_pass(TransformVisitor))
269}
270
271// An example to test plugin transform.
272// Recommended strategy to test plugin's transform is verify
273// the Visitor's behavior, instead of trying to run `process_transform` with mocks
274// unless explicitly required to do so.
275test_inline!(
276    Default::default(),
277    |_| visit_mut_pass(TransformVisitor),
278    boo,
279    // Input codes
280    r#"console.log("transform");"#,
281    // Output codes after transformed with plugin
282    r#"console.log("transform");"#
283);"##
284                .as_bytes(),
285        )
286        .context("failed to write the rust source file")?;
287
288        println!(
289            r#"✅ Successfully created {}.
290If you haven't, please ensure to add target via "rustup target add {}" "#,
291            path.display(),
292            build_target
293        );
294        Ok(())
295    }
296}
297
298/// Set of subcommands for the plugin subcommand.
299#[derive(Subcommand)]
300pub enum PluginSubcommand {
301    /// Create a new plugin project with minimal scaffolding template.
302    New(PluginScaffoldOptions),
303}