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