1use std::{cell::RefCell, fs, path::PathBuf, sync::Arc};
2
3use napi::bindgen_prelude::*;
4use napi_derive::napi;
5use swc_core::{
6 base::{
7 config::{IsModule, ParseOptions},
8 try_with_handler,
9 },
10 common::{
11 FileName, FilePathMapping, GLOBALS, Mark, SourceMap, SyntaxContext, errors::ColorConfig,
12 },
13 ecma::{
14 ast::{Decl, EsVersion, Id},
15 atoms::Atom,
16 parser::{EsSyntax, Syntax, TsSyntax},
17 utils::{ExprCtx, find_pat_ids},
18 visit::{Visit, VisitMutWith, VisitWith},
19 },
20};
21
22use crate::{
23 next_api::utils::{NapiIssueSourceRange, NapiSourcePos},
24 util::MapErr,
25};
26
27struct Finder {
28 pub named_exports: Vec<Atom>,
29}
30
31impl Visit for Finder {
32 fn visit_export_decl(&mut self, node: &swc_core::ecma::ast::ExportDecl) {
33 match &node.decl {
34 Decl::Class(class_decl) => {
35 self.named_exports.push(class_decl.ident.sym.clone());
36 }
37 Decl::Fn(fn_decl) => {
38 self.named_exports.push(fn_decl.ident.sym.clone());
39 }
40 Decl::Var(var_decl) => {
41 let ids: Vec<Id> = find_pat_ids(&var_decl.decls);
42 for id in ids {
43 self.named_exports.push(id.0);
44 }
45 }
46 _ => {}
47 }
48 }
49
50 fn visit_export_named_specifier(&mut self, node: &swc_core::ecma::ast::ExportNamedSpecifier) {
51 let named_export = if let Some(exported) = &node.exported {
52 exported.atom().clone()
53 } else {
54 node.orig.atom().clone()
55 };
56 self.named_exports.push(named_export);
57 }
58
59 fn visit_export_namespace_specifier(
60 &mut self,
61 node: &swc_core::ecma::ast::ExportNamespaceSpecifier,
62 ) {
63 self.named_exports.push(node.name.atom().clone());
64 }
65}
66
67pub struct FinderTask {
68 pub resource_path: Option<String>,
69}
70
71impl Task for FinderTask {
72 type Output = Vec<Atom>;
73 type JsValue = Array;
74
75 fn compute(&mut self) -> napi::Result<Self::Output> {
76 let resource_path = PathBuf::from(self.resource_path.take().unwrap());
77 let src = fs::read_to_string(&resource_path)
78 .map_err(|e| napi::Error::from_reason(e.to_string()))?;
79
80 let syntax = match resource_path
81 .extension()
82 .map(|os_str| os_str.to_string_lossy())
83 {
84 Some(ext) if matches!(ext.as_ref(), "ts" | "mts" | "cts") => {
85 Syntax::Typescript(TsSyntax {
86 tsx: false,
87 decorators: true,
88 dts: false,
89 no_early_errors: true,
90 disallow_ambiguous_jsx_like: false,
91 })
92 }
93 Some(ext) if matches!(ext.as_ref(), "tsx" | "mtsx" | "ctsx") => {
94 Syntax::Typescript(TsSyntax {
95 tsx: true,
96 decorators: true,
97 dts: false,
98 no_early_errors: true,
99 disallow_ambiguous_jsx_like: false,
100 })
101 }
102 _ => Syntax::Es(EsSyntax {
103 jsx: true,
104 fn_bind: true,
105 decorators: true,
106 decorators_before_export: true,
107 export_default_from: true,
108 import_attributes: true,
109 allow_super_outside_method: true,
110 allow_return_outside_function: true,
111 auto_accessors: true,
112 explicit_resource_management: true,
113 }),
114 };
115
116 GLOBALS.set(&Default::default(), || {
117 let c =
118 swc_core::base::Compiler::new(Arc::new(SourceMap::new(FilePathMapping::empty())));
119
120 let options = ParseOptions {
121 comments: false,
122 syntax,
123 is_module: IsModule::Unknown,
124 target: EsVersion::default(),
125 };
126 let fm =
127 c.cm.new_source_file(Arc::new(FileName::Real(resource_path)), src);
128 let program = try_with_handler(
129 c.cm.clone(),
130 swc_core::base::HandlerOpts {
131 color: ColorConfig::Never,
132 skip_filename: false,
133 },
134 |handler| {
135 c.parse_js(
136 fm,
137 handler,
138 options.target,
139 options.syntax,
140 options.is_module,
141 None,
142 )
143 },
144 )
145 .map_err(|e| e.to_pretty_error())
146 .convert_err()?;
147
148 let mut visitor = Finder {
149 named_exports: Vec::new(),
150 };
151 program.visit_with(&mut visitor);
153
154 Ok(visitor.named_exports)
155 })
156 }
157
158 fn resolve(&mut self, env: Env, result: Self::Output) -> napi::Result<Self::JsValue> {
159 let mut array = env.create_array(result.len() as u32)?;
160 for (i, name) in result.iter().enumerate() {
161 let js_val = env.create_string(name.as_str())?;
162 array.set(i as u32, js_val)?;
163 }
164 Ok(array)
165 }
166}
167
168#[napi(ts_return_type = "Promise<string[]>")]
169pub fn get_module_named_exports(resource_path: String) -> AsyncTask<FinderTask> {
170 AsyncTask::new(FinderTask {
171 resource_path: Some(resource_path),
172 })
173}
174
175#[napi(object)]
176pub struct NapiSourceDiagnostic {
177 pub severity: &'static str,
178 pub message: String,
179 pub loc: NapiIssueSourceRange,
180}
181
182pub struct AnalyzeTask {
183 pub source: Option<String>,
184 pub is_production: bool,
185}
186
187impl Task for AnalyzeTask {
188 type Output = Vec<NapiSourceDiagnostic>;
189 type JsValue = Vec<NapiSourceDiagnostic>;
190
191 fn compute(&mut self) -> Result<Self::Output> {
192 GLOBALS.set(&Default::default(), || {
193 let c =
194 swc_core::base::Compiler::new(Arc::new(SourceMap::new(FilePathMapping::empty())));
195
196 let options = ParseOptions {
197 comments: false,
198 syntax: Syntax::Es(EsSyntax {
199 jsx: true,
200 fn_bind: true,
201 decorators: true,
202 decorators_before_export: true,
203 export_default_from: true,
204 import_attributes: true,
205 allow_super_outside_method: true,
206 allow_return_outside_function: true,
207 auto_accessors: true,
208 explicit_resource_management: true,
209 }),
210 is_module: IsModule::Unknown,
211 target: EsVersion::default(),
212 };
213 let source = self.source.take().unwrap();
214 let fm =
215 c.cm.new_source_file(Arc::new(FileName::Anon), source);
216 let mut program = try_with_handler(
217 c.cm.clone(),
218 swc_core::base::HandlerOpts {
219 color: ColorConfig::Never,
220 skip_filename: false,
221 },
222 |handler| {
223 c.parse_js(
224 fm,
225 handler,
226 options.target,
227 options.syntax,
228 options.is_module,
229 None,
230 )
231 },
232 )
233 .map_err(|e| e.to_pretty_error())
234 .convert_err()?;
235
236 let diagnostics = RefCell::new(Vec::new());
237 let top_level_mark = Mark::fresh(Mark::root());
238 let unresolved_mark = Mark::fresh(Mark::root());
239 let mut resolver_visitor = swc_core::ecma::transforms::base::resolver(unresolved_mark, top_level_mark, true);
240 let mut analyze_visitor = next_custom_transforms::transforms::warn_for_edge_runtime::warn_for_edge_runtime_with_handlers(
241 c.cm.clone(),
242 ExprCtx {
243 is_unresolved_ref_safe: true,
244 unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
245 in_strict: false,
246 remaining_depth: 4,
247 },
248 false,
249 self.is_production,
250 |span, msg| {
251 let start = c.cm.lookup_char_pos(span.lo);
252 let end = c.cm.lookup_char_pos(span.hi);
253 diagnostics.borrow_mut().push(NapiSourceDiagnostic {
254 severity: "Warning",
255 message: msg,
256 loc: NapiIssueSourceRange {
257 start: NapiSourcePos {
258 line: start.line as u32,
259 column: start.col_display as u32,
260 },
261 end: NapiSourcePos {
262 line: end.line as u32,
263 column: end.col_display as u32,
264 }
265 }
266 });
267 },
268 |span, msg| {
269 let start = c.cm.lookup_char_pos(span.lo);
270 let end = c.cm.lookup_char_pos(span.hi);
271 diagnostics.borrow_mut().push(NapiSourceDiagnostic {
272 severity: "Error",
273 message: msg,
274 loc: NapiIssueSourceRange {
275 start: NapiSourcePos {
276 line: start.line as u32,
277 column: start.col_display as u32,
278 },
279 end: NapiSourcePos {
280 line: end.line as u32,
281 column: end.col_display as u32,
282 }
283 }
284 });
285 });
286
287 program.visit_mut_with(&mut resolver_visitor);
288 program.visit_with(&mut analyze_visitor);
289
290 Ok(diagnostics.take())
291 })
292 }
293
294 fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
295 Ok(output)
296 }
297}
298
299#[napi(ts_return_type = "Promise<NapiSourceDiagnostic[]>")]
300pub fn warn_for_edge_runtime(source: String, is_production: bool) -> AsyncTask<AnalyzeTask> {
301 AsyncTask::new(AnalyzeTask {
302 source: Some(source),
303 is_production,
304 })
305}