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