Skip to main content

wasm/
lib.rs

1use std::{fmt::Debug, sync::Arc};
2
3use anyhow::Context;
4use js_sys::JsString;
5use next_custom_transforms::chain_transforms::{TransformOptions, custom_before_pass};
6use rustc_hash::FxHashMap;
7use swc_core::{
8    base::{
9        Compiler,
10        config::{JsMinifyOptions, ParseOptions},
11        try_with_handler,
12    },
13    common::{
14        FileName, FilePathMapping, GLOBALS, Mark, SourceMap,
15        comments::{Comments, SingleThreadedComments},
16        errors::ColorConfig,
17    },
18    ecma::ast::noop_pass,
19};
20use wasm_bindgen::prelude::*;
21use wasm_bindgen_futures::future_to_promise;
22
23pub mod mdx;
24
25fn convert_err(err: impl Debug) -> JsError {
26    JsError::new(&format!("{err:?}"))
27}
28
29#[wasm_bindgen(js_name = "minifySync")]
30pub fn minify_sync(s: JsString, opts: JsValue) -> Result<JsValue, JsValue> {
31    console_error_panic_hook::set_once();
32
33    let c = compiler();
34
35    let opts: JsMinifyOptions = serde_wasm_bindgen::from_value(opts)?;
36
37    let value = try_with_handler(
38        c.cm.clone(),
39        swc_core::base::HandlerOpts {
40            color: ColorConfig::Never,
41            skip_filename: false,
42        },
43        |handler| {
44            GLOBALS.set(&Default::default(), || {
45                let fm = c.cm.new_source_file(FileName::Anon.into(), String::from(s));
46                let program = c
47                    .minify(fm, handler, &opts, Default::default())
48                    .context("failed to minify file")?;
49
50                Ok(program)
51            })
52        },
53    )
54    .map_err(|e| e.to_pretty_error())
55    .map_err(convert_err)?;
56
57    Ok(serde_wasm_bindgen::to_value(&value)?)
58}
59
60#[wasm_bindgen(js_name = "minify")]
61pub fn minify(s: JsString, opts: JsValue) -> js_sys::Promise {
62    // TODO: This'll be properly scheduled once wasm have standard backed thread
63    // support.
64    future_to_promise(async { minify_sync(s, opts) })
65}
66
67#[wasm_bindgen(js_name = "transformSync")]
68pub fn transform_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsError> {
69    console_error_panic_hook::set_once();
70
71    let c = compiler();
72    let mut opts: TransformOptions = serde_wasm_bindgen::from_value(opts)?;
73
74    let s = s.dyn_into::<js_sys::JsString>();
75    let out = try_with_handler(
76        c.cm.clone(),
77        swc_core::base::HandlerOpts {
78            color: ColorConfig::Never,
79            skip_filename: false,
80        },
81        |handler| {
82            GLOBALS.set(&Default::default(), || {
83                let unresolved_mark = Mark::new();
84                opts.swc.unresolved_mark = Some(unresolved_mark);
85
86                let out = match s {
87                    Ok(s) => {
88                        let fm = c.cm.new_source_file(
89                            if opts.swc.filename.is_empty() {
90                                FileName::Anon.into()
91                            } else {
92                                FileName::Real(opts.swc.filename.clone().into()).into()
93                            },
94                            String::from(s),
95                        );
96                        let cm = c.cm.clone();
97                        let file = fm.clone();
98                        let comments = SingleThreadedComments::default();
99                        c.process_js_with_custom_pass(
100                            fm,
101                            None,
102                            handler,
103                            &opts.swc,
104                            comments.clone(),
105                            |_| {
106                                custom_before_pass(
107                                    cm,
108                                    file,
109                                    &opts,
110                                    comments.clone(),
111                                    Default::default(),
112                                    unresolved_mark,
113                                    Default::default(),
114                                )
115                            },
116                            |_| noop_pass(),
117                        )
118                        .context("failed to process js file")?
119                    }
120                    Err(v) => c.process_js(
121                        handler,
122                        serde_wasm_bindgen::from_value(v).expect(""),
123                        &opts.swc,
124                    )?,
125                };
126
127                Ok(out)
128            })
129        },
130    )
131    .map_err(|e| e.to_pretty_error())
132    .map_err(convert_err)?;
133
134    Ok(serde_wasm_bindgen::to_value(&out)?)
135}
136
137#[wasm_bindgen(js_name = "transform")]
138pub fn transform(s: JsValue, opts: JsValue) -> js_sys::Promise {
139    // TODO: This'll be properly scheduled once wasm have standard backed thread
140    // support.
141    future_to_promise(async { Ok(transform_sync(s, opts)?) })
142}
143
144#[wasm_bindgen(js_name = "parseSync")]
145pub fn parse_sync(s: JsString, opts: JsValue) -> Result<JsValue, JsError> {
146    console_error_panic_hook::set_once();
147
148    let c = swc_core::base::Compiler::new(Arc::new(SourceMap::new(FilePathMapping::empty())));
149    let opts: ParseOptions = serde_wasm_bindgen::from_value(opts)?;
150
151    try_with_handler(
152        c.cm.clone(),
153        swc_core::base::HandlerOpts {
154            ..Default::default()
155        },
156        |handler| {
157            c.run(|| {
158                GLOBALS.set(&Default::default(), || {
159                    let fm = c.cm.new_source_file(FileName::Anon.into(), String::from(s));
160
161                    let cmts = c.comments().clone();
162                    let comments = if opts.comments {
163                        Some(&cmts as &dyn Comments)
164                    } else {
165                        None
166                    };
167
168                    let program = c
169                        .parse_js(
170                            fm,
171                            handler,
172                            opts.target,
173                            opts.syntax,
174                            opts.is_module,
175                            comments,
176                        )
177                        .context("failed to parse code")?;
178
179                    let s = serde_json::to_string(&program).unwrap();
180                    Ok(JsValue::from_str(&s))
181                })
182            })
183        },
184    )
185    .map_err(|e| e.to_pretty_error())
186    .map_err(convert_err)
187}
188
189#[wasm_bindgen(js_name = "parse")]
190pub fn parse(s: JsString, opts: JsValue) -> js_sys::Promise {
191    // TODO: This'll be properly scheduled once wasm have standard backed thread
192    // support.
193    future_to_promise(async { Ok(parse_sync(s, opts)?) })
194}
195
196/// Get global sourcemap
197fn compiler() -> Arc<Compiler> {
198    let cm = Arc::new(SourceMap::new(FilePathMapping::empty()));
199
200    Arc::new(Compiler::new(cm))
201}
202
203#[wasm_bindgen(js_name = "expandNextJsTemplate")]
204pub fn expand_next_js_template(
205    content: Box<[u8]>,
206    template_path: &str,
207    next_package_dir_path: &str,
208    replacements: JsValue,
209    injections: JsValue,
210    imports: JsValue,
211) -> Result<String, JsError> {
212    next_taskless::expand_next_js_template(
213        str::from_utf8(&content).map_err(convert_err)?,
214        template_path,
215        next_package_dir_path,
216        serde_wasm_bindgen::from_value::<FxHashMap<String, String>>(replacements)?
217            .iter()
218            .map(|(k, v)| (&**k, &**v)),
219        serde_wasm_bindgen::from_value::<FxHashMap<String, String>>(injections)?
220            .iter()
221            .map(|(k, v)| (&**k, &**v)),
222        serde_wasm_bindgen::from_value::<FxHashMap<String, Option<String>>>(imports)?
223            .iter()
224            .map(|(k, v)| (&**k, v.as_deref())),
225    )
226    .map_err(convert_err)
227}
228
229#[wasm_bindgen(js_name = "codeFrameColumns")]
230pub fn code_frame_columns(
231    source: Box<[u8]>,
232    location: JsValue,
233    options: JsValue,
234) -> Result<Option<String>, JsError> {
235    console_error_panic_hook::set_once();
236
237    let location: next_code_frame::CodeFrameLocation = serde_wasm_bindgen::from_value(location)?;
238    let options: next_code_frame::CodeFrameOptions = serde_wasm_bindgen::from_value(options)?;
239    next_code_frame::render_code_frame(
240        str::from_utf8(&source)
241            .map_err(|e| JsError::new(&format!("Failed to render code frame: {e}")))?,
242        &location,
243        &options,
244    )
245    .map_err(|e| JsError::new(&format!("Failed to render code frame: {e}")))
246}