next_swc_napi/
rspack.rs

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