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