1use 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#[derive(Clone, Debug)]
29pub struct JS {
30 source_code: Rc<String>,
31}
32
33impl JS {
34 pub fn from_string(source_code: String) -> JS {
36 JS {
37 source_code: Rc::new(source_code),
38 }
39 }
40
41 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 pub fn as_bytes(&self) -> &[u8] {
52 self.source_code.as_bytes()
53 }
54
55 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 pub(crate) fn exports(&self) -> Result<Vec<String>> {
71 let module = self.parse_module()?;
72
73 let mut functions = HashMap::new();
75 let mut named_exports = vec![];
77 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}