Skip to main content

next_napi_bindings/
transform.rs

1/*
2Copyright (c) 2017 The swc Project Developers
3
4Permission is hereby granted, free of charge, to any
5person obtaining a copy of this software and associated
6documentation files (the "Software"), to deal in the
7Software without restriction, including without
8limitation the rights to use, copy, modify, merge,
9publish, distribute, sublicense, and/or sell copies of
10the Software, and to permit persons to whom the Software
11is furnished to do so, subject to the following
12conditions:
13
14The above copyright notice and this permission notice
15shall be included in all copies or substantial portions
16of the Software.
17
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
19ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
20TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
21PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
22SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
25IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26DEALINGS IN THE SOFTWARE.
27*/
28
29use std::{
30    cell::RefCell,
31    fs::read_to_string,
32    panic::{AssertUnwindSafe, catch_unwind},
33    rc::Rc,
34    sync::{Arc, LazyLock},
35};
36
37use anyhow::{Context as _, anyhow, bail};
38use napi::bindgen_prelude::*;
39use napi_derive::napi;
40use next_custom_transforms::chain_transforms::{TransformOptions, custom_before_pass};
41use rustc_hash::{FxHashMap, FxHashSet};
42use swc_core::{
43    atoms::Atom,
44    base::{Compiler, TransformOutput, config::RuntimeOptions, try_with_handler},
45    common::{FileName, GLOBALS, Mark, comments::SingleThreadedComments, errors::ColorConfig},
46    ecma::ast::noop_pass,
47};
48use swc_plugin_backend_wasmtime::WasmtimeRuntime;
49
50use crate::{complete_output, get_compiler, util::MapErr};
51
52/// Input to transform
53#[derive(Debug)]
54pub enum Input {
55    /// Raw source code.
56    Source { src: String },
57    /// Get source code from filename in options
58    FromFilename,
59}
60
61pub struct TransformTask {
62    pub c: Compiler,
63    pub input: Input,
64    pub options: Buffer,
65}
66
67fn skip_filename() -> bool {
68    fn check(name: &str) -> bool {
69        let v = std::env::var(name);
70        let v = match v {
71            Ok(v) => v,
72            Err(_) => return false,
73        };
74
75        !v.is_empty() && v != "0"
76    }
77
78    static SKIP_FILENAME: LazyLock<bool> = LazyLock::new(|| {
79        check("NEXT_TEST_MODE") || check("__NEXT_TEST_MODE") || check("NEXT_TEST_JOB")
80    });
81
82    *SKIP_FILENAME
83}
84
85impl Task for TransformTask {
86    type Output = (TransformOutput, FxHashSet<Atom>, FxHashMap<String, usize>);
87    type JsValue = Object;
88
89    fn compute(&mut self) -> napi::Result<Self::Output> {
90        GLOBALS.set(&Default::default(), || {
91            let eliminated_packages: Rc<RefCell<FxHashSet<Atom>>> = Default::default();
92            let use_cache_telemetry_tracker: Rc<RefCell<FxHashMap<String, usize>>> =
93                Default::default();
94
95            let res = catch_unwind(AssertUnwindSafe(|| {
96                try_with_handler(
97                    self.c.cm.clone(),
98                    swc_core::base::HandlerOpts {
99                        color: ColorConfig::Always,
100                        skip_filename: skip_filename(),
101                    },
102                    |handler| {
103                        self.c.run(|| {
104                            let options: TransformOptions = serde_json::from_slice(&self.options)?;
105                            let fm = match &self.input {
106                                Input::Source { src } => {
107                                    let filename = if options.swc.filename.is_empty() {
108                                        FileName::Anon
109                                    } else {
110                                        FileName::Real(options.swc.filename.clone().into())
111                                    };
112
113                                    self.c.cm.new_source_file(filename.into(), src.to_string())
114                                }
115                                Input::FromFilename => {
116                                    let filename = &options.swc.filename;
117                                    if filename.is_empty() {
118                                        bail!("no filename is provided via options");
119                                    }
120
121                                    self.c.cm.new_source_file(
122                                        FileName::Real(filename.into()).into(),
123                                        read_to_string(filename).with_context(|| {
124                                            format!("Failed to read source code from {filename}")
125                                        })?,
126                                    )
127                                }
128                            };
129                            let unresolved_mark = Mark::new();
130                            let mut options = options.patch(&fm);
131                            options.swc.unresolved_mark = Some(unresolved_mark);
132                            options.swc.runtime_options =
133                                RuntimeOptions::default().plugin_runtime(Arc::new(WasmtimeRuntime));
134
135                            let cm = self.c.cm.clone();
136                            let file = fm.clone();
137
138                            let comments = SingleThreadedComments::default();
139                            self.c.process_js_with_custom_pass(
140                                fm,
141                                None,
142                                handler,
143                                &options.swc,
144                                comments.clone(),
145                                |_| {
146                                    custom_before_pass(
147                                        cm,
148                                        file,
149                                        &options,
150                                        comments.clone(),
151                                        eliminated_packages.clone(),
152                                        unresolved_mark,
153                                        use_cache_telemetry_tracker.clone(),
154                                    )
155                                },
156                                |_| noop_pass(),
157                            )
158                        })
159                    },
160                )
161            }))
162            .map_err(|err| {
163                if let Some(s) = err.downcast_ref::<String>() {
164                    anyhow!("failed to process {}", s)
165                } else {
166                    anyhow!("failed to process")
167                }
168            });
169
170            match res {
171                Ok(res) => res
172                    .map(|o| {
173                        (
174                            o,
175                            eliminated_packages.replace(Default::default()),
176                            Rc::into_inner(use_cache_telemetry_tracker)
177                                .expect(
178                                    "All other copies of use_cache_telemetry_tracker should be \
179                                     dropped by this point",
180                                )
181                                .into_inner(),
182                        )
183                    })
184                    .map_err(|e| e.to_pretty_error())
185                    .convert_err(),
186                Err(err) => Err(napi::Error::new(Status::GenericFailure, format!("{err:?}"))),
187            }
188        })
189    }
190
191    fn resolve(
192        &mut self,
193        env: Env,
194        (output, eliminated_packages, use_cache_telemetry_tracker): Self::Output,
195    ) -> napi::Result<Self::JsValue> {
196        complete_output(
197            &env,
198            output,
199            eliminated_packages,
200            use_cache_telemetry_tracker,
201        )
202    }
203}
204
205#[napi]
206pub fn transform(
207    src: Either3<String, Buffer, Undefined>,
208    _is_module: bool,
209    options: Buffer,
210    signal: Option<AbortSignal>,
211) -> napi::Result<AsyncTask<TransformTask>> {
212    let c = get_compiler();
213
214    let input = match src {
215        Either3::A(src) => Input::Source { src },
216        Either3::B(src) => Input::Source {
217            src: String::from_utf8_lossy(&src).to_string(),
218        },
219        Either3::C(_) => Input::FromFilename,
220    };
221
222    let task = TransformTask { c, input, options };
223    Ok(AsyncTask::with_optional_signal(task, signal))
224}
225
226#[napi]
227pub fn transform_sync(
228    env: Env,
229    src: Either3<String, Buffer, Undefined>,
230    _is_module: bool,
231    options: Buffer,
232) -> napi::Result<Object> {
233    let c = get_compiler();
234
235    let input = match src {
236        Either3::A(src) => Input::Source { src },
237        Either3::B(src) => Input::Source {
238            src: String::from_utf8_lossy(&src).to_string(),
239        },
240        Either3::C(_) => Input::FromFilename,
241    };
242
243    let mut task = TransformTask { c, input, options };
244    let output = task.compute()?;
245    task.resolve(env, output)
246}
247#[test]
248fn test_deser() {
249    const JSON_STR: &str = r#"{"jsc":{"parser":{"syntax":"ecmascript","dynamicImport":true,"jsx":true},"transform":{"react":{"runtime":"automatic","pragma":"React.createElement","pragmaFrag":"React.Fragment","throwIfNamespace":true,"development":false,"useBuiltins":true}},"target":"es5"},"filename":"/Users/timneutkens/projects/next.js/packages/next/dist/client/next.js","sourceMaps":false,"sourceFileName":"/Users/timneutkens/projects/next.js/packages/next/dist/client/next.js"}"#;
250
251    let tr: TransformOptions = serde_json::from_str(JSON_STR).unwrap();
252
253    println!("{tr:#?}");
254}
255
256#[test]
257fn test_deserialize_transform_regenerator() {
258    const JSON_STR: &str = r#"{"jsc":{"parser":{"syntax":"ecmascript","dynamicImport":true,"jsx":true},"transform":{ "regenerator": { "importPath": "foo" }, "react":{"runtime":"automatic","pragma":"React.createElement","pragmaFrag":"React.Fragment","throwIfNamespace":true,"development":false,"useBuiltins":true}},"target":"es5"},"filename":"/Users/timneutkens/projects/next.js/packages/next/dist/client/next.js","sourceMaps":false,"sourceFileName":"/Users/timneutkens/projects/next.js/packages/next/dist/client/next.js"}"#;
259
260    let tr: TransformOptions = serde_json::from_str(JSON_STR).unwrap();
261
262    println!("{tr:#?}");
263}