wasm/
lib.rs

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