1use std::{
7 collections::HashMap,
8 fs::File,
9 io::{Cursor, Read},
10 path::Path,
11};
12
13use anyhow::{Context, Result, anyhow, bail};
14use brotli::enc::{self, BrotliEncoderParams};
15use swc_core::{
16 common::{FileName, SourceMap},
17 ecma::{
18 ast::{
19 Decl, EsVersion, ExportDecl, ExportSpecifier, Module, ModuleDecl, ModuleExportName,
20 ModuleItem, Stmt,
21 },
22 parser::{self, EsSyntax, Syntax},
23 },
24};
25
26#[derive(Clone, Debug)]
28pub struct JS {
29 source_code: String,
30}
31
32impl JS {
33 pub fn from_string(source_code: String) -> JS {
35 JS { source_code }
36 }
37
38 pub fn from_file(path: &Path) -> Result<JS> {
40 let mut input_file = File::open(path)
41 .with_context(|| format!("Failed to open input file {}", path.display()))?;
42 let mut contents: Vec<u8> = vec![];
43 input_file.read_to_end(&mut contents)?;
44 Ok(Self::from_string(String::from_utf8(contents)?))
45 }
46
47 pub fn as_bytes(&self) -> &[u8] {
49 self.source_code.as_bytes()
50 }
51
52 pub(crate) fn compress(&self) -> Result<Vec<u8>> {
54 let mut compressed_source_code: Vec<u8> = vec![];
55 enc::BrotliCompress(
56 &mut Cursor::new(&self.source_code.as_bytes()),
57 &mut compressed_source_code,
58 &BrotliEncoderParams {
59 quality: 11,
60 ..Default::default()
61 },
62 )?;
63 Ok(compressed_source_code)
64 }
65
66 pub(crate) fn exports(&self) -> Result<Vec<String>> {
68 let module = self.parse_module()?;
69
70 let mut functions = HashMap::new();
72 let mut named_exports = vec![];
74 let mut exported_functions = vec![];
76 for item in module.body {
77 match item {
78 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
79 decl: Decl::Fn(f),
80 ..
81 })) => {
82 if !f.function.params.is_empty() {
83 bail!("Exported functions with parameters are not supported");
84 }
85 if f.function.is_generator {
86 bail!("Exported generators are not supported");
87 }
88 exported_functions.push(f.ident.sym);
89 }
90 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
91 for specifier in e.specifiers {
92 if let ExportSpecifier::Named(n) = specifier {
93 let orig = match n.orig {
94 ModuleExportName::Ident(i) => i.sym,
95 ModuleExportName::Str(s) => s.value.to_atom_lossy().into_owned(),
96 };
97 let exported_name = n.exported.map(|e| match e {
98 ModuleExportName::Ident(i) => i.sym,
99 ModuleExportName::Str(s) => s.value.to_atom_lossy().into_owned(),
100 });
101 named_exports.push((orig, exported_name));
102 }
103 }
104 }
105 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(e)) if e.decl.is_fn_expr() => {
106 exported_functions.push("default".into())
107 }
108 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(e)) if e.expr.is_arrow() => {
109 exported_functions.push("default".into())
110 }
111 ModuleItem::Stmt(Stmt::Decl(Decl::Fn(f))) => {
112 functions.insert(
113 f.ident.sym,
114 (f.function.params.is_empty(), f.function.is_generator),
115 );
116 }
117 _ => continue,
118 }
119 }
120
121 let mut named_exported_functions = named_exports
122 .into_iter()
123 .filter_map(|(orig, exported)| {
124 if let Some((no_params, is_generator)) = functions.get(&orig) {
125 if !no_params {
126 Some(Err(anyhow!(
127 "Exported functions with parameters are not supported"
128 )))
129 } else if *is_generator {
130 Some(Err(anyhow!("Exported generators are not supported")))
131 } else {
132 Some(Ok(exported.unwrap_or(orig)))
133 }
134 } else {
135 None
136 }
137 })
138 .collect::<Result<Vec<_>, _>>()?;
139 exported_functions.append(&mut named_exported_functions);
140 Ok(exported_functions
141 .into_iter()
142 .map(|f| f.to_string())
143 .collect())
144 }
145
146 fn parse_module(&self) -> Result<Module> {
147 let source_map: SourceMap = Default::default();
148 let file = source_map.new_source_file(FileName::Anon.into(), self.source_code.to_owned());
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}