#![allow(clippy::trailing_empty_array)] mod module_lexer; use std::sync::Arc; use flexbuffers::FlexbufferSerializer; use napi::bindgen_prelude::Buffer; use napi_derive::napi; use serde::Serialize; use oxc_allocator::Allocator; pub use oxc_ast::ast::Program; use oxc_ast::CommentKind; use oxc_diagnostics::miette::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 { pub r#type: &'static str, #[napi(ts_type = "'Line' | 'Block'")] 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() .map(|name| SourceType::from_path(name).unwrap()) .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); } /// # 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(); 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.unwrap_or_default(); let source = Arc::new(NamedSource::new(file_name, source_text.to_string())); ret.errors .into_iter() .map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source))) .map(|error| format!("{error:?}")) .collect() }; let comments = ret .trivias .comments() .map(|(kind, span)| Comment { r#type: match kind { CommentKind::SingleLine => "Line", CommentKind::MultiLine => "Block", }, value: span.source_text(&source_text).to_string(), start: span.start, end: span.end, }) .collect::>(); ParseResult { program, comments, errors } } /// Returns a binary AST in flexbuffers format. /// This is a POC API. Error handling is not done yet. /// /// # Panics /// /// * File extension is invalid /// * FlexbufferSerializer serialization error #[allow(clippy::needless_pass_by_value)] #[napi] pub fn parse_sync_buffer(source_text: String, options: Option) -> Buffer { let options = options.unwrap_or_default(); let allocator = Allocator::default(); let ret = parse(&allocator, &source_text, &options); let mut serializer = FlexbufferSerializer::new(); ret.program.serialize(&mut serializer).unwrap(); serializer.take_buffer().into() } /// # Panics /// /// * Tokio crashes #[allow(clippy::needless_pass_by_value)] #[napi] pub async fn parse_async(source_text: String, options: Option) -> ParseResult { tokio::spawn(async move { parse_sync(source_text, options) }).await.unwrap() }