1use 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#[derive(Debug)]
52pub enum Input {
53 Source { src: String },
55 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}