mod module_lexer; use std::sync::Arc; use napi::{bindgen_prelude::AsyncTask, Task}; use napi_derive::napi; use oxc_allocator::Allocator; pub use oxc_ast::ast::Program; use oxc_ast::CommentKind; use oxc_diagnostics::{Error, NamedSource}; use oxc_parser::{Parser, ParserReturn}; use oxc_span::SourceType; pub use crate::module_lexer::*; /// Babel Parser Options /// /// #[napi(object)] #[derive(Default)] pub struct ParserOptions { #[napi(ts_type = "'script' | 'module' | 'unambiguous' | undefined")] pub source_type: Option, pub source_filename: Option, /// Emit `ParenthesizedExpression` in AST. /// /// If this option is true, parenthesized expressions are represented by /// (non-standard) `ParenthesizedExpression` nodes that have a single `expression` property /// containing the expression inside parentheses. /// /// Default: true pub preserve_parens: Option, } #[napi(object)] pub struct ParseResult { pub program: String, pub comments: Vec, pub errors: Vec, } #[napi(object)] pub struct Comment { #[napi(ts_type = "'Line' | 'Block'")] pub r#type: &'static str, pub value: String, pub start: u32, pub end: u32, } fn parse<'a>( allocator: &'a Allocator, source_text: &'a str, options: &ParserOptions, ) -> ParserReturn<'a> { let source_type = options .source_filename .as_ref() .and_then(|name| SourceType::from_path(name).ok()) .unwrap_or_default(); let source_type = match options.source_type.as_deref() { Some("script") => source_type.with_script(true), Some("module") => source_type.with_module(true), _ => source_type, }; Parser::new(allocator, source_text, source_type) .preserve_parens(options.preserve_parens.unwrap_or(true)) .parse() } /// Parse without returning anything. /// This is for benchmark purposes such as measuring napi communication overhead. /// /// # Panics /// /// * File extension is invalid /// * Serde JSON serialization #[allow(clippy::needless_pass_by_value)] #[napi] pub fn parse_without_return(source_text: String, options: Option) { let options = options.unwrap_or_default(); let allocator = Allocator::default(); parse(&allocator, &source_text, &options); } #[allow(clippy::needless_lifetimes)] fn parse_with_return<'a>(source_text: &'a str, options: &ParserOptions) -> ParseResult { let allocator = Allocator::default(); let ret = parse(&allocator, source_text, options); let program = serde_json::to_string(&ret.program).unwrap(); let errors = if ret.errors.is_empty() { vec![] } else { let file_name = options.source_filename.clone().unwrap_or_default(); let source = Arc::new(NamedSource::new(file_name, source_text.to_string())); ret.errors .into_iter() .map(|diagnostic| Error::from(diagnostic).with_source_code(Arc::clone(&source))) .map(|error| format!("{error:?}")) .collect() }; let comments = ret .trivias .comments() .map(|comment| Comment { r#type: match comment.kind { CommentKind::SingleLine => "Line", CommentKind::MultiLine => "Block", }, value: comment.span.source_text(source_text).to_string(), start: comment.span.start, end: comment.span.end, }) .collect::>(); ParseResult { program, comments, errors } } /// # Panics /// /// * File extension is invalid /// * Serde JSON serialization #[allow(clippy::needless_pass_by_value)] #[napi] pub fn parse_sync(source_text: String, options: Option) -> ParseResult { let options = options.unwrap_or_default(); parse_with_return(&source_text, &options) } pub struct ResolveTask { source_text: String, options: ParserOptions, } #[napi] impl Task for ResolveTask { type Output = ParseResult; type JsValue = ParseResult; fn compute(&mut self) -> napi::Result { Ok(parse_with_return(&self.source_text, &self.options)) } fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result { Ok(result) } } /// # Panics /// /// * Tokio crashes #[allow(clippy::needless_pass_by_value)] #[napi] pub fn parse_async(source_text: String, options: Option) -> AsyncTask { let options = options.unwrap_or_default(); AsyncTask::new(ResolveTask { source_text, options }) }