javy_codegen/
js.rs

1/// Higher-level representation of JavaScript.
2///
3/// This is intended to be used to derive different representations of source
4/// code. For example, as a byte array, a string, QuickJS bytecode, compressed
5/// bytes, or attributes of the source code like what it exports.
6use std::{
7    collections::HashMap,
8    fs::File,
9    io::{Cursor, Read},
10    path::Path,
11    rc::Rc,
12};
13
14use anyhow::{anyhow, bail, Context, Result};
15use brotli::enc::{self, BrotliEncoderParams};
16use swc_core::{
17    common::{FileName, SourceMap},
18    ecma::{
19        ast::{
20            Decl, EsVersion, ExportDecl, ExportSpecifier, Module, ModuleDecl, ModuleExportName,
21            ModuleItem, Stmt,
22        },
23        parser::{self, EsSyntax, Syntax},
24    },
25};
26
27/// JS source code.
28#[derive(Clone, Debug)]
29pub struct JS {
30    source_code: Rc<String>,
31}
32
33impl JS {
34    /// Create [`JS`] from a string containing JS source code.
35    pub fn from_string(source_code: String) -> JS {
36        JS {
37            source_code: Rc::new(source_code),
38        }
39    }
40
41    /// Create [`JS`] from a file containing JS.
42    pub fn from_file(path: &Path) -> Result<JS> {
43        let mut input_file = File::open(path)
44            .with_context(|| format!("Failed to open input file {}", path.display()))?;
45        let mut contents: Vec<u8> = vec![];
46        input_file.read_to_end(&mut contents)?;
47        Ok(Self::from_string(String::from_utf8(contents)?))
48    }
49
50    /// Get source code as bytes.
51    pub fn as_bytes(&self) -> &[u8] {
52        self.source_code.as_bytes()
53    }
54
55    /// Get Brotli compressed JS source code as bytes.
56    pub(crate) fn compress(&self) -> Result<Vec<u8>> {
57        let mut compressed_source_code: Vec<u8> = vec![];
58        enc::BrotliCompress(
59            &mut Cursor::new(&self.source_code.as_bytes()),
60            &mut compressed_source_code,
61            &BrotliEncoderParams {
62                quality: 11,
63                ..Default::default()
64            },
65        )?;
66        Ok(compressed_source_code)
67    }
68
69    /// Get the exports from a JS instance.
70    pub(crate) fn exports(&self) -> Result<Vec<String>> {
71        let module = self.parse_module()?;
72
73        // function foo() ...
74        let mut functions = HashMap::new();
75        // export { foo, bar as baz }
76        let mut named_exports = vec![];
77        // export function foo() ...
78        let mut exported_functions = vec![];
79        for item in module.body {
80            match item {
81                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
82                    decl: Decl::Fn(f),
83                    ..
84                })) => {
85                    if !f.function.params.is_empty() {
86                        bail!("Exported functions with parameters are not supported");
87                    }
88                    if f.function.is_generator {
89                        bail!("Exported generators are not supported");
90                    }
91                    exported_functions.push(f.ident.sym);
92                }
93                ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
94                    for specifier in e.specifiers {
95                        if let ExportSpecifier::Named(n) = specifier {
96                            let orig = match n.orig {
97                                ModuleExportName::Ident(i) => i.sym,
98                                ModuleExportName::Str(s) => s.value,
99                            };
100                            let exported_name = n.exported.map(|e| match e {
101                                ModuleExportName::Ident(i) => i.sym,
102                                ModuleExportName::Str(s) => s.value,
103                            });
104                            named_exports.push((orig, exported_name));
105                        }
106                    }
107                }
108                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(e)) if e.decl.is_fn_expr() => {
109                    exported_functions.push("default".into())
110                }
111                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(e)) if e.expr.is_arrow() => {
112                    exported_functions.push("default".into())
113                }
114                ModuleItem::Stmt(Stmt::Decl(Decl::Fn(f))) => {
115                    functions.insert(
116                        f.ident.sym,
117                        (f.function.params.is_empty(), f.function.is_generator),
118                    );
119                }
120                _ => continue,
121            }
122        }
123
124        let mut named_exported_functions = named_exports
125            .into_iter()
126            .filter_map(|(orig, exported)| {
127                if let Some((no_params, is_generator)) = functions.get(&orig) {
128                    if !no_params {
129                        Some(Err(anyhow!(
130                            "Exported functions with parameters are not supported"
131                        )))
132                    } else if *is_generator {
133                        Some(Err(anyhow!("Exported generators are not supported")))
134                    } else {
135                        Some(Ok(exported.unwrap_or(orig)))
136                    }
137                } else {
138                    None
139                }
140            })
141            .collect::<Result<Vec<_>, _>>()?;
142        exported_functions.append(&mut named_exported_functions);
143        Ok(exported_functions
144            .into_iter()
145            .map(|f| f.to_string())
146            .collect())
147    }
148
149    fn parse_module(&self) -> Result<Module> {
150        let source_map: SourceMap = Default::default();
151        let file = source_map.new_source_file_from(FileName::Anon.into(), self.source_code.clone());
152        let mut errors = vec![];
153        parser::parse_file_as_module(
154            &file,
155            Syntax::Es(EsSyntax::default()),
156            EsVersion::Es2020,
157            None,
158            &mut errors,
159        )
160        .map_err(|e| anyhow!(e.into_kind().msg()))
161        .with_context(|| "Invalid JavaScript")
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use anyhow::Result;
168
169    use crate::js::JS;
170
171    #[test]
172    fn parse_no_exports() -> Result<()> {
173        let exports = parse("function foo() {}")?;
174        assert_eq!(Vec::<&str>::default(), exports);
175        Ok(())
176    }
177
178    #[test]
179    fn parse_invalid_js() -> Result<()> {
180        let res = parse("fun foo() {}");
181        assert_eq!("Invalid JavaScript", res.err().unwrap().to_string());
182        Ok(())
183    }
184
185    #[test]
186    fn parse_one_func_export() -> Result<()> {
187        let exports = parse("export function foo() {}")?;
188        assert_eq!(vec!["foo"], exports);
189        Ok(())
190    }
191
192    #[test]
193    fn parse_func_export_with_parameter() -> Result<()> {
194        let res = parse("export function foo(bar) {}");
195        assert_eq!(
196            "Exported functions with parameters are not supported",
197            res.err().unwrap().to_string()
198        );
199        Ok(())
200    }
201
202    #[test]
203    fn parse_generator_export() -> Result<()> {
204        let res = parse("export function *foo() {}");
205        assert_eq!(
206            "Exported generators are not supported",
207            res.err().unwrap().to_string()
208        );
209        Ok(())
210    }
211
212    #[test]
213    fn parse_two_func_exports() -> Result<()> {
214        let exports = parse("export function foo() {}; export function bar() {};")?;
215        assert_eq!(vec!["foo", "bar"], exports);
216        Ok(())
217    }
218
219    #[test]
220    fn parse_const_export() -> Result<()> {
221        let exports = parse("export const x = 1;")?;
222        let expected_exports: Vec<&str> = vec![];
223        assert_eq!(expected_exports, exports);
224        Ok(())
225    }
226
227    #[test]
228    fn parse_const_export_and_func_export() -> Result<()> {
229        let exports = parse("export const x = 1; export function foo() {}")?;
230        assert_eq!(vec!["foo"], exports);
231        Ok(())
232    }
233
234    #[test]
235    fn parse_named_func_export() -> Result<()> {
236        let exports = parse("function foo() {}; export { foo };")?;
237        assert_eq!(vec!["foo"], exports);
238        Ok(())
239    }
240
241    #[test]
242    fn parse_named_func_export_with_arg() -> Result<()> {
243        let res = parse("function foo(bar) {}; export { foo };");
244        assert_eq!(
245            "Exported functions with parameters are not supported",
246            res.err().unwrap().to_string()
247        );
248        Ok(())
249    }
250
251    #[test]
252    fn parse_funcs_with_args() -> Result<()> {
253        let exports = parse("function foo(bar) {}")?;
254        assert_eq!(Vec::<&str>::default(), exports);
255        Ok(())
256    }
257
258    #[test]
259    fn parse_named_func_export_and_const_export() -> Result<()> {
260        let exports = parse("function foo() {}; const bar = 1; export { foo, bar };")?;
261        assert_eq!(vec!["foo"], exports);
262        Ok(())
263    }
264
265    #[test]
266    fn parse_func_export_and_named_func_export() -> Result<()> {
267        let exports = parse("export function foo() {}; function bar() {}; export { bar };")?;
268        assert_eq!(vec!["foo", "bar"], exports);
269        Ok(())
270    }
271
272    #[test]
273    fn parse_renamed_func_export() -> Result<()> {
274        let exports = parse("function foo() {}; export { foo as bar };")?;
275        assert_eq!(vec!["bar"], exports);
276        Ok(())
277    }
278
279    #[test]
280    fn parse_hoisted_func_export() -> Result<()> {
281        let exports = parse("export { foo }; function foo() {}")?;
282        assert_eq!(vec!["foo"], exports);
283        Ok(())
284    }
285
286    #[test]
287    fn parse_renamed_hosted_func_export() -> Result<()> {
288        let exports = parse("export { foo as bar }; function foo() {}")?;
289        assert_eq!(vec!["bar"], exports);
290        Ok(())
291    }
292
293    #[test]
294    fn parse_hoisted_exports_with_func_and_const() -> Result<()> {
295        let exports = parse("export { foo, bar }; function foo() {}; const bar = 1;")?;
296        assert_eq!(vec!["foo"], exports);
297        Ok(())
298    }
299
300    #[test]
301    fn parse_default_arrow_export() -> Result<()> {
302        let exports = parse("export default () => {}")?;
303        assert_eq!(vec!["default"], exports);
304        Ok(())
305    }
306
307    #[test]
308    fn parse_default_function_export() -> Result<()> {
309        let exports = parse("export default function() {}")?;
310        assert_eq!(vec!["default"], exports);
311        Ok(())
312    }
313
314    fn parse(js: &str) -> Result<Vec<String>> {
315        JS::from_string(js.to_string()).exports()
316    }
317}