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 Wasm32UnknownUnknown,
15 Wasm32Wasip1,
17}
18
19#[derive(Parser, Debug)]
20pub struct PluginScaffoldOptions {
21 #[clap(long)]
23 pub name: Option<String>,
24
25 #[clap(long, arg_enum)]
34 pub target_type: PluginTargetType,
35
36 pub path: PathBuf,
37}
38
39fn 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
60fn 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 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 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 base_git_cmd
137 .args(["init", name])
138 .output()
139 .context("failed to create dir for the plugin")?;
140
141 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 let swc_core_version = format!("{}.{}.*", swc_core_version[0], swc_core_version[1]);
148
149 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 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 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 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#[derive(Subcommand)]
300pub enum PluginSubcommand {
301 New(PluginScaffoldOptions),
303}