next_swc_napi/
rspack.rs

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            // Visit the AST to find named exports
152            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}