next_swc_napi/
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};
35
36use anyhow::{Context as _, anyhow, bail};
37use napi::bindgen_prelude::*;
38use next_custom_transforms::chain_transforms::{TransformOptions, custom_before_pass};
39use once_cell::sync::Lazy;
40use rustc_hash::{FxHashMap, FxHashSet};
41use swc_core::{
42    atoms::Atom,
43    base::{Compiler, TransformOutput, try_with_handler},
44    common::{FileName, GLOBALS, Mark, comments::SingleThreadedComments, errors::ColorConfig},
45    ecma::ast::noop_pass,
46};
47
48use crate::{complete_output, get_compiler, util::MapErr};
49
50/// Input to transform
51#[derive(Debug)]
52pub enum Input {
53    /// Raw source code.
54    Source { src: String },
55    /// Get source code from filename in options
56    FromFilename,
57}
58
59pub struct TransformTask {
60    pub c: Compiler,
61    pub input: Input,
62    pub options: Buffer,
63}
64
65fn skip_filename() -> bool {
66    fn check(name: &str) -> bool {
67        let v = std::env::var(name);
68        let v = match v {
69            Ok(v) => v,
70            Err(_) => return false,
71        };
72
73        !v.is_empty() && v != "0"
74    }
75
76    static SKIP_FILENAME: Lazy<bool> = Lazy::new(|| {
77        check("NEXT_TEST_MODE") || check("__NEXT_TEST_MODE") || check("NEXT_TEST_JOB")
78    });
79
80    *SKIP_FILENAME
81}
82
83impl Task for TransformTask {
84    type Output = (TransformOutput, FxHashSet<Atom>, FxHashMap<String, usize>);
85    type JsValue = Object;
86
87    fn compute(&mut self) -> napi::Result<Self::Output> {
88        GLOBALS.set(&Default::default(), || {
89            let eliminated_packages: Rc<RefCell<FxHashSet<Atom>>> = Default::default();
90            let use_cache_telemetry_tracker: Rc<RefCell<FxHashMap<String, usize>>> =
91                Default::default();
92
93            let res = catch_unwind(AssertUnwindSafe(|| {
94                try_with_handler(
95                    self.c.cm.clone(),
96                    swc_core::base::HandlerOpts {
97                        color: ColorConfig::Always,
98                        skip_filename: skip_filename(),
99                    },
100                    |handler| {
101                        self.c.run(|| {
102                            let options: TransformOptions = serde_json::from_slice(&self.options)?;
103                            let fm = match &self.input {
104                                Input::Source { src } => {
105                                    let filename = if options.swc.filename.is_empty() {
106                                        FileName::Anon
107                                    } else {
108                                        FileName::Real(options.swc.filename.clone().into())
109                                    };
110
111                                    self.c.cm.new_source_file(filename.into(), src.to_string())
112                                }
113                                Input::FromFilename => {
114                                    let filename = &options.swc.filename;
115                                    if filename.is_empty() {
116                                        bail!("no filename is provided via options");
117                                    }
118
119                                    self.c.cm.new_source_file(
120                                        FileName::Real(filename.into()).into(),
121                                        read_to_string(filename).with_context(|| {
122                                            format!("Failed to read source code from {filename}")
123                                        })?,
124                                    )
125                                }
126                            };
127                            let unresolved_mark = Mark::new();
128                            let mut options = options.patch(&fm);
129                            options.swc.unresolved_mark = Some(unresolved_mark);
130
131                            let cm = self.c.cm.clone();
132                            let file = fm.clone();
133
134                            let comments = SingleThreadedComments::default();
135                            self.c.process_js_with_custom_pass(
136                                fm,
137                                None,
138                                handler,
139                                &options.swc,
140                                comments.clone(),
141                                |_| {
142                                    custom_before_pass(
143                                        cm,
144                                        file,
145                                        &options,
146                                        comments.clone(),
147                                        eliminated_packages.clone(),
148                                        unresolved_mark,
149                                        use_cache_telemetry_tracker.clone(),
150                                    )
151                                },
152                                |_| noop_pass(),
153                            )
154                        })
155                    },
156                )
157            }))
158            .map_err(|err| {
159                if let Some(s) = err.downcast_ref::<String>() {
160                    anyhow!("failed to process {}", s)
161                } else {
162                    anyhow!("failed to process")
163                }
164            });
165
166            match res {
167                Ok(res) => res
168                    .map(|o| {
169                        (
170                            o,
171                            eliminated_packages.replace(Default::default()),
172                            Rc::into_inner(use_cache_telemetry_tracker)
173                                .expect(
174                                    "All other copies of use_cache_telemetry_tracker should be \
175                                     dropped by this point",
176                                )
177                                .into_inner(),
178                        )
179                    })
180                    .map_err(|e| e.to_pretty_error())
181                    .convert_err(),
182                Err(err) => Err(napi::Error::new(Status::GenericFailure, format!("{err:?}"))),
183            }
184        })
185    }
186
187    fn resolve(
188        &mut self,
189        env: Env,
190        (output, eliminated_packages, use_cache_telemetry_tracker): Self::Output,
191    ) -> napi::Result<Self::JsValue> {
192        complete_output(
193            &env,
194            output,
195            eliminated_packages,
196            use_cache_telemetry_tracker,
197        )
198    }
199}
200
201#[napi]
202pub fn transform(
203    src: Either3<String, Buffer, Undefined>,
204    _is_module: bool,
205    options: Buffer,
206    signal: Option<AbortSignal>,
207) -> napi::Result<AsyncTask<TransformTask>> {
208    let c = get_compiler();
209
210    let input = match src {
211        Either3::A(src) => Input::Source { src },
212        Either3::B(src) => Input::Source {
213            src: String::from_utf8_lossy(&src).to_string(),
214        },
215        Either3::C(_) => Input::FromFilename,
216    };
217
218    let task = TransformTask { c, input, options };
219    Ok(AsyncTask::with_optional_signal(task, signal))
220}
221
222#[napi]
223pub fn transform_sync(
224    env: Env,
225    src: Either3<String, Buffer, Undefined>,
226    _is_module: bool,
227    options: Buffer,
228) -> napi::Result<Object> {
229    let c = get_compiler();
230
231    let input = match src {
232        Either3::A(src) => Input::Source { src },
233        Either3::B(src) => Input::Source {
234            src: String::from_utf8_lossy(&src).to_string(),
235        },
236        Either3::C(_) => Input::FromFilename,
237    };
238
239    let mut task = TransformTask { c, input, options };
240    let output = task.compute()?;
241    task.resolve(env, output)
242}
243#[test]
244fn test_deser() {
245    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"}"#;
246
247    let tr: TransformOptions = serde_json::from_str(JSON_STR).unwrap();
248
249    println!("{tr:#?}");
250}
251
252#[test]
253fn test_deserialize_transform_regenerator() {
254    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"}"#;
255
256    let tr: TransformOptions = serde_json::from_str(JSON_STR).unwrap();
257
258    println!("{tr:#?}");
259}