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, debug_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            use swc_core::plugin_runner::plugin_module_bytes::{
32                CompiledPluginModuleBytes, RawPluginModuleBytes,
33            };
34            use swc_plugin_backend_wasmer::WasmerRuntime;
35
36            Self(CompiledPluginModuleBytes::from_raw_module(
37                &WasmerRuntime,
38                RawPluginModuleBytes::new(plugin_name.to_string(), plugin_bytes),
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            };
136            use swc_plugin_backend_wasmer::WasmerRuntime;
137            use turbo_tasks::TryJoinIterExt;
138
139            let plugins = self
140                .plugins
141                .iter()
142                .map(async |(plugin_module, config)| {
143                    let plugin_module = plugin_module.await?;
144                    Ok((
145                        config.clone(),
146                        Box::new(plugin_module.0.clone_module(&WasmerRuntime)),
147                    ))
148                })
149                .try_join()
150                .await?;
151
152            let should_enable_comments_proxy =
153                !ctx.comments.leading.is_empty() && !ctx.comments.trailing.is_empty();
154
155            //[TODO]: as same as swc/core does, we should set should_enable_comments_proxy
156            // depends on the src's comments availability. For now, check naively if leading
157            // / trailing comments are empty.
158            let comments = if should_enable_comments_proxy {
159                // Plugin only able to accept singlethreaded comments, interop from
160                // multithreaded comments.
161                let mut leading =
162                    swc_core::common::comments::SingleThreadedCommentsMapInner::default();
163                ctx.comments.leading.as_ref().into_iter().for_each(|c| {
164                    leading.insert(*c.key(), c.value().clone());
165                });
166
167                let mut trailing =
168                    swc_core::common::comments::SingleThreadedCommentsMapInner::default();
169                ctx.comments.trailing.as_ref().into_iter().for_each(|c| {
170                    trailing.insert(*c.key(), c.value().clone());
171                });
172
173                Some(SingleThreadedComments::from_leading_and_trailing(
174                    Rc::new(RefCell::new(leading)),
175                    Rc::new(RefCell::new(trailing)),
176                ))
177            } else {
178                None
179            };
180
181            let transformed_program =
182                COMMENTS.set(&HostCommentsStorage { inner: comments }, || {
183                    let module_program =
184                        std::mem::replace(program, Program::Module(Module::dummy()));
185                    let module_program =
186                        swc_core::common::plugin::serialized::VersionedSerializable::new(
187                            module_program,
188                        );
189                    let mut serialized_program =
190                        PluginSerializedBytes::try_serialize(&module_program)?;
191
192                    let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
193                        Some(ctx.file_path_str.to_string()),
194                        //[TODO]: Support env-related variable injection, i.e process.env.NODE_ENV
195                        "development".to_string(),
196                        None,
197                    ));
198
199                    // Run plugin transformation against current program.
200                    // We do not serialize / deserialize between each plugin execution but
201                    // copies raw transformed bytes directly into plugin's memory space.
202                    // Note: This doesn't mean plugin won't perform any se/deserialization: it
203                    // still have to construct from raw bytes internally to perform actual
204                    // transform.
205                    for (plugin_config, plugin_module) in plugins {
206                        let mut transform_plugin_executor =
207                            swc_core::plugin_runner::create_plugin_transform_executor(
208                                ctx.source_map,
209                                &ctx.unresolved_mark,
210                                &transform_metadata_context,
211                                None,
212                                plugin_module,
213                                Some(plugin_config),
214                                Arc::new(WasmerRuntime),
215                            );
216
217                        serialized_program = transform_plugin_executor
218                            .transform(&serialized_program, Some(should_enable_comments_proxy))?;
219                    }
220
221                    serialized_program.deserialize().map(|v| v.into_inner())
222                })?;
223
224            *program = transformed_program;
225        }
226
227        #[cfg(not(feature = "swc_ecma_transform_plugin"))]
228        {
229            use turbopack_core::issue::IssueExt;
230
231            UnsupportedSwcEcmaTransformPluginsIssue {
232                file_path: ctx.file_path.clone(),
233            }
234            .resolved_cell()
235            .emit();
236        }
237
238        Ok(())
239    }
240}