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