swc/
plugin.rs

1//! This module always exists because cfg attributes are not stabilized in
2//! expressions at the moment.
3
4#![cfg_attr(
5    any(not(any(feature = "plugin")), target_arch = "wasm32"),
6    allow(unused)
7)]
8
9use std::{path::PathBuf, sync::Arc};
10
11use anyhow::{Context, Result};
12use atoms::Atom;
13use common::FileName;
14use serde::{Deserialize, Serialize};
15use swc_common::errors::{DiagnosticId, HANDLER};
16use swc_ecma_ast::Pass;
17#[cfg(feature = "plugin")]
18use swc_ecma_ast::*;
19use swc_ecma_loader::{
20    resolve::Resolve,
21    resolvers::{lru::CachingResolver, node::NodeModulesResolver},
22};
23use swc_ecma_visit::{fold_pass, noop_fold_type, Fold};
24#[cfg(feature = "plugin")]
25use swc_plugin_runner::runtime::Runtime as PluginRuntime;
26
27/// A tuple represents a plugin.
28///
29/// First element is a resolvable name to the plugin, second is a JSON object
30/// that represents configuration option for those plugin.
31/// Type of plugin's configuration is up to each plugin - swc/core does not have
32/// strong type and it'll be serialized into plain string when it's passed to
33/// plugin's entrypoint function.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(deny_unknown_fields, rename_all = "camelCase")]
36pub struct PluginConfig(pub String, pub serde_json::Value);
37
38#[cfg(feature = "plugin")]
39pub(crate) fn plugins(
40    configured_plugins: Option<Vec<PluginConfig>>,
41    plugin_env_vars: Option<Vec<Atom>>,
42    metadata_context: std::sync::Arc<swc_common::plugin::metadata::TransformPluginMetadataContext>,
43    comments: Option<swc_common::comments::SingleThreadedComments>,
44    source_map: std::sync::Arc<swc_common::SourceMap>,
45    unresolved_mark: swc_common::Mark,
46    plugin_runtime: Arc<dyn PluginRuntime>,
47) -> impl Pass {
48    fold_pass(RustPlugins {
49        plugins: configured_plugins,
50        plugin_env_vars: plugin_env_vars.map(std::sync::Arc::new),
51        metadata_context,
52        comments,
53        source_map,
54        unresolved_mark,
55        plugin_runtime,
56    })
57}
58
59#[cfg(feature = "plugin")]
60struct RustPlugins {
61    plugins: Option<Vec<PluginConfig>>,
62    plugin_env_vars: Option<std::sync::Arc<Vec<Atom>>>,
63    metadata_context: std::sync::Arc<swc_common::plugin::metadata::TransformPluginMetadataContext>,
64    comments: Option<swc_common::comments::SingleThreadedComments>,
65    source_map: std::sync::Arc<swc_common::SourceMap>,
66    unresolved_mark: swc_common::Mark,
67    plugin_runtime: Arc<dyn PluginRuntime>,
68}
69
70#[cfg(feature = "plugin")]
71impl RustPlugins {
72    #[cfg(feature = "plugin")]
73    fn apply(&mut self, n: Program) -> Result<Program, anyhow::Error> {
74        use anyhow::Context;
75        if self.plugins.is_none() || self.plugins.as_ref().unwrap().is_empty() {
76            return Ok(n);
77        }
78
79        let filename = self.metadata_context.filename.clone();
80
81        if cfg!(feature = "manual-tokio-runtime") {
82            self.apply_inner(n)
83        } else {
84            let fut = async move { self.apply_inner(n) };
85            if let Ok(handle) = tokio::runtime::Handle::try_current() {
86                handle.block_on(fut)
87            } else {
88                tokio::runtime::Runtime::new().unwrap().block_on(fut)
89            }
90        }
91        .with_context(|| format!("failed to invoke plugin on '{filename:?}'"))
92    }
93
94    #[tracing::instrument(level = "info", skip_all, name = "apply_plugins")]
95    #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))]
96    fn apply_inner(&mut self, n: Program) -> Result<Program, anyhow::Error> {
97        use anyhow::Context;
98        use swc_common::plugin::serialized::PluginSerializedBytes;
99
100        // swc_plugin_macro will not inject proxy to the comments if comments is empty
101        let should_enable_comments_proxy = self.comments.is_some();
102
103        // Set comments once per whole plugin transform execution.
104        swc_plugin_proxy::COMMENTS.set(
105            &swc_plugin_proxy::HostCommentsStorage {
106                inner: self.comments.clone(),
107            },
108            || {
109                let span = tracing::span!(tracing::Level::INFO, "serialize_program").entered();
110                let program = swc_common::plugin::serialized::VersionedSerializable::new(n);
111                let mut serialized = PluginSerializedBytes::try_serialize(&program)?;
112                drop(span);
113
114                // Run plugin transformation against current program.
115                // We do not serialize / deserialize between each plugin execution but
116                // copies raw transformed bytes directly into plugin's memory space.
117                // Note: This doesn't mean plugin won't perform any se/deserialization: it
118                // still have to construct from raw bytes internally to perform actual
119                // transform.
120                if let Some(plugins) = &mut self.plugins {
121                    for p in plugins.drain(..) {
122                        let plugin_module_bytes = crate::config::PLUGIN_MODULE_CACHE
123                            .inner
124                            .get()
125                            .unwrap()
126                            .lock()
127                            .get(&*self.plugin_runtime, &p.0)
128                            .expect("plugin module should be loaded");
129
130                        let plugin_name = plugin_module_bytes.get_module_name().to_string();
131
132                        let mut transform_plugin_executor =
133                            swc_plugin_runner::create_plugin_transform_executor(
134                                &self.source_map,
135                                &self.unresolved_mark,
136                                &self.metadata_context,
137                                self.plugin_env_vars.clone(),
138                                plugin_module_bytes,
139                                Some(p.1),
140                                self.plugin_runtime.clone(),
141                            );
142
143                        let span = tracing::span!(
144                            tracing::Level::INFO,
145                            "execute_plugin_runner",
146                            plugin_module = p.0.as_str()
147                        )
148                        .entered();
149
150                        serialized = transform_plugin_executor
151                            .transform(&serialized, Some(should_enable_comments_proxy))
152                            .with_context(|| {
153                                format!(
154                                    "failed to invoke `{}` as js transform plugin at {}",
155                                    &p.0, plugin_name
156                                )
157                            })?;
158                        drop(span);
159                    }
160                }
161
162                // Plugin transformation is done. Deserialize transformed bytes back
163                // into Program
164                serialized.deserialize().map(|v| v.into_inner())
165            },
166        )
167    }
168
169    #[cfg(all(feature = "plugin", target_arch = "wasm32"))]
170    #[tracing::instrument(level = "info", skip_all)]
171    fn apply_inner(&mut self, n: Program) -> Result<Program, anyhow::Error> {
172        // [TODO]: unimplemented
173        n
174    }
175}
176
177#[cfg(feature = "plugin")]
178impl Fold for RustPlugins {
179    noop_fold_type!();
180
181    fn fold_module(&mut self, n: Module) -> Module {
182        match self.apply(Program::Module(n)) {
183            Ok(program) => program.expect_module(),
184            Err(err) => {
185                HANDLER.with(|handler| {
186                    handler.err_with_code(&err.to_string(), DiagnosticId::Error("plugin".into()));
187                });
188                Module::default()
189            }
190        }
191    }
192
193    fn fold_script(&mut self, n: Script) -> Script {
194        match self.apply(Program::Script(n)) {
195            Ok(program) => program.expect_script(),
196            Err(err) => {
197                HANDLER.with(|handler| {
198                    handler.err_with_code(&err.to_string(), DiagnosticId::Error("plugin".into()));
199                });
200                Script::default()
201            }
202        }
203    }
204}
205
206#[cfg(feature = "plugin")]
207pub(crate) fn compile_wasm_plugins(
208    cache_root: Option<&str>,
209    plugins: &[PluginConfig],
210    #[cfg(feature = "plugin")] plugin_runtime: &dyn PluginRuntime,
211) -> Result<()> {
212    let plugin_resolver = CachingResolver::new(
213        40,
214        NodeModulesResolver::new(swc_ecma_loader::TargetEnv::Node, Default::default(), true),
215    );
216
217    // Currently swc enables filesystemcache by default on Embedded runtime plugin
218    // target.
219    crate::config::init_plugin_module_cache_once(true, cache_root);
220
221    let mut inner_cache = crate::config::PLUGIN_MODULE_CACHE
222        .inner
223        .get()
224        .expect("Cache should be available")
225        .lock();
226
227    // Populate cache to the plugin modules if not loaded
228    for plugin_config in plugins.iter() {
229        let plugin_name = &plugin_config.0;
230
231        if !inner_cache.contains(plugin_runtime, plugin_name) {
232            let resolved_path = plugin_resolver
233                .resolve(&FileName::Real(PathBuf::from(plugin_name)), plugin_name)
234                .with_context(|| format!("failed to resolve plugin path: {plugin_name}"))?;
235
236            let path = if let FileName::Real(value) = resolved_path.filename {
237                value
238            } else {
239                anyhow::bail!("Failed to resolve plugin path: {:?}", resolved_path);
240            };
241
242            inner_cache.store_bytes_from_path(plugin_runtime, &path, plugin_name)?;
243            tracing::debug!("Initialized WASM plugin {plugin_name}");
244        }
245    }
246
247    Ok(())
248}