turbopack_ecmascript_plugins/transform/
swc_ecma_transform_plugins.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use swc_core::ecma::ast::Program;
4use turbo_rcstr::rcstr;
5use turbo_tasks::Vc;
6use turbo_tasks_fs::FileSystemPath;
7use turbopack_core::issue::{Issue, IssueSeverity, IssueStage, OptionStyledString, StyledString};
8use turbopack_ecmascript::{CustomTransformer, TransformContext};
9
10/// A wrapper around an SWC's ecma transform wasm plugin module bytes, allowing
11/// it to operate with the turbo_tasks caching requirements.
12///
13/// Internally this contains a `CompiledPluginModuleBytes`, which points to the
14/// compiled, serialized wasmer::Module instead of raw file bytes to reduce the
15/// cost of the compilation.
16#[turbo_tasks::value(serialization = "none", eq = "manual", into = "new", cell = "new")]
17pub struct SwcPluginModule(
18    #[turbo_tasks(trace_ignore)]
19    #[cfg(feature = "swc_ecma_transform_plugin")]
20    pub swc_core::plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
21    // Dummy field to avoid turbo_tasks macro complaining about empty struct.
22    // This is because we can't import CompiledPluginModuleBytes by default, it should be only
23    // available for the target / platforms that support swc plugins (which can build wasmer)
24    #[cfg(not(feature = "swc_ecma_transform_plugin"))] pub (),
25);
26
27impl SwcPluginModule {
28    pub fn new(plugin_name: &str, plugin_bytes: Vec<u8>) -> Self {
29        #[cfg(feature = "swc_ecma_transform_plugin")]
30        {
31            Self({
32                use swc_core::plugin_runner::plugin_module_bytes::{
33                    CompiledPluginModuleBytes, RawPluginModuleBytes,
34                };
35                CompiledPluginModuleBytes::from(RawPluginModuleBytes::new(
36                    plugin_name.to_string(),
37                    plugin_bytes,
38                ))
39            })
40        }
41
42        #[cfg(not(feature = "swc_ecma_transform_plugin"))]
43        {
44            let _ = plugin_name;
45            let _ = plugin_bytes;
46            Self(())
47        }
48    }
49}
50
51#[turbo_tasks::value(shared)]
52struct UnsupportedSwcEcmaTransformPluginsIssue {
53    pub file_path: FileSystemPath,
54}
55
56#[turbo_tasks::value_impl]
57impl Issue for UnsupportedSwcEcmaTransformPluginsIssue {
58    fn severity(&self) -> IssueSeverity {
59        IssueSeverity::Warning
60    }
61
62    #[turbo_tasks::function]
63    fn stage(&self) -> Vc<IssueStage> {
64        IssueStage::Transform.cell()
65    }
66
67    #[turbo_tasks::function]
68    fn title(&self) -> Vc<StyledString> {
69        StyledString::Text(rcstr!(
70            "Unsupported SWC EcmaScript transform plugins on this platform."
71        ))
72        .cell()
73    }
74
75    #[turbo_tasks::function]
76    fn file_path(&self) -> Vc<FileSystemPath> {
77        self.file_path.clone().cell()
78    }
79
80    #[turbo_tasks::function]
81    fn description(&self) -> Vc<OptionStyledString> {
82        Vc::cell(Some(
83            StyledString::Text(rcstr!(
84                "Turbopack does not yet support running SWC EcmaScript transform plugins on this \
85                 platform."
86            ))
87            .resolved_cell(),
88        ))
89    }
90}
91
92/// A custom transformer plugin to execute SWC's transform plugins.
93#[derive(Debug)]
94pub struct SwcEcmaTransformPluginsTransformer {
95    #[cfg(feature = "swc_ecma_transform_plugin")]
96    plugins: Vec<(turbo_tasks::ResolvedVc<SwcPluginModule>, serde_json::Value)>,
97}
98
99impl SwcEcmaTransformPluginsTransformer {
100    #[cfg(feature = "swc_ecma_transform_plugin")]
101    pub fn new(
102        plugins: Vec<(turbo_tasks::ResolvedVc<SwcPluginModule>, serde_json::Value)>,
103    ) -> Self {
104        Self { plugins }
105    }
106
107    // [TODO] Due to WEB-1102 putting this module itself behind compile time feature
108    // doesn't work. Instead allow to instantiate dummy instance.
109    #[cfg(not(feature = "swc_ecma_transform_plugin"))]
110    #[allow(clippy::new_without_default)]
111    pub fn new() -> Self {
112        Self {}
113    }
114}
115
116#[async_trait]
117impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
118    #[cfg_attr(not(feature = "swc_ecma_transform_plugin"), allow(unused))]
119    #[tracing::instrument(level = tracing::Level::TRACE, name = "swc_ecma_transform_plugin", skip_all)]
120    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
121        #[cfg(feature = "swc_ecma_transform_plugin")]
122        {
123            use std::{cell::RefCell, rc::Rc, sync::Arc};
124
125            use swc_core::{
126                common::{
127                    comments::SingleThreadedComments,
128                    plugin::{
129                        metadata::TransformPluginMetadataContext, serialized::PluginSerializedBytes,
130                    },
131                    util::take::Take,
132                },
133                ecma::ast::Module,
134                plugin::proxies::{COMMENTS, HostCommentsStorage},
135                plugin_runner::plugin_module_bytes::PluginModuleBytes,
136            };
137
138            let mut plugins = vec![];
139            for (plugin_module, config) in &self.plugins {
140                let plugin_module = &plugin_module.await?.0;
141
142                plugins.push((
143                    plugin_module.get_module_name().to_string(),
144                    config.clone(),
145                    Box::new(plugin_module.clone()),
146                ));
147            }
148
149            let should_enable_comments_proxy =
150                !ctx.comments.leading.is_empty() && !ctx.comments.trailing.is_empty();
151
152            //[TODO]: as same as swc/core does, we should set should_enable_comments_proxy
153            // depends on the src's comments availability. For now, check naively if leading
154            // / trailing comments are empty.
155            let comments = if should_enable_comments_proxy {
156                // Plugin only able to accept singlethreaded comments, interop from
157                // multithreaded comments.
158                let mut leading =
159                    swc_core::common::comments::SingleThreadedCommentsMapInner::default();
160                ctx.comments.leading.as_ref().into_iter().for_each(|c| {
161                    leading.insert(*c.key(), c.value().clone());
162                });
163
164                let mut trailing =
165                    swc_core::common::comments::SingleThreadedCommentsMapInner::default();
166                ctx.comments.trailing.as_ref().into_iter().for_each(|c| {
167                    trailing.insert(*c.key(), c.value().clone());
168                });
169
170                Some(SingleThreadedComments::from_leading_and_trailing(
171                    Rc::new(RefCell::new(leading)),
172                    Rc::new(RefCell::new(trailing)),
173                ))
174            } else {
175                None
176            };
177
178            let transformed_program =
179                COMMENTS.set(&HostCommentsStorage { inner: comments }, || {
180                    let module_program =
181                        std::mem::replace(program, Program::Module(Module::dummy()));
182                    let module_program =
183                        swc_core::common::plugin::serialized::VersionedSerializable::new(
184                            module_program,
185                        );
186                    let mut serialized_program =
187                        PluginSerializedBytes::try_serialize(&module_program)?;
188
189                    let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
190                        Some(ctx.file_path_str.to_string()),
191                        //[TODO]: Support env-related variable injection, i.e process.env.NODE_ENV
192                        "development".to_string(),
193                        None,
194                    ));
195
196                    // Run plugin transformation against current program.
197                    // We do not serialize / deserialize between each plugin execution but
198                    // copies raw transformed bytes directly into plugin's memory space.
199                    // Note: This doesn't mean plugin won't perform any se/deserialization: it
200                    // still have to construct from raw bytes internally to perform actual
201                    // transform.
202                    for (_plugin_name, plugin_config, plugin_module) in plugins.drain(..) {
203                        let runtime =
204                            swc_core::plugin_runner::wasix_runtime::build_wasi_runtime(None);
205                        let mut transform_plugin_executor =
206                            swc_core::plugin_runner::create_plugin_transform_executor(
207                                ctx.source_map,
208                                &ctx.unresolved_mark,
209                                &transform_metadata_context,
210                                None,
211                                plugin_module,
212                                Some(plugin_config),
213                                runtime,
214                            );
215
216                        serialized_program = transform_plugin_executor
217                            .transform(&serialized_program, Some(should_enable_comments_proxy))?;
218                    }
219
220                    serialized_program.deserialize().map(|v| v.into_inner())
221                })?;
222
223            *program = transformed_program;
224        }
225
226        #[cfg(not(feature = "swc_ecma_transform_plugin"))]
227        {
228            use turbopack_core::issue::IssueExt;
229
230            UnsupportedSwcEcmaTransformPluginsIssue {
231                file_path: ctx.file_path.clone(),
232            }
233            .resolved_cell()
234            .emit();
235        }
236
237        Ok(())
238    }
239}