oxc/crates/oxc_wasm/src/lib.rs
Boshen 7fa2fa386c
refactor(wasm): clean up code and add transform (#5299)
Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
2024-08-28 22:14:35 +08:00

416 lines
14 KiB
Rust

// Silence erroneous warnings from Rust Analyser for `#[derive(Tsify)]`
#![allow(non_snake_case)]
mod options;
use std::{
cell::{Cell, RefCell},
path::{Path, PathBuf},
rc::Rc,
};
use oxc::{
allocator::Allocator,
ast::{ast::Program, CommentKind, Trivias, Visit},
codegen::{CodeGenerator, CodegenOptions},
diagnostics::Error,
minifier::{CompressOptions, Minifier, MinifierOptions},
parser::{ParseOptions, Parser, ParserReturn},
semantic::{ScopeFlags, ScopeId, ScopeTree, SemanticBuilder, SymbolTable},
span::SourceType,
transformer::{TransformOptions, Transformer},
};
use oxc_index::Idx;
use oxc_linter::Linter;
use oxc_prettier::{Prettier, PrettierOptions};
use serde::Serialize;
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use crate::options::{OxcOptions, OxcRunOptions};
#[wasm_bindgen(getter_with_clone)]
#[derive(Default, Tsify)]
#[serde(rename_all = "camelCase")]
pub struct Oxc {
#[wasm_bindgen(readonly, skip_typescript)]
#[tsify(type = "Program")]
pub ast: JsValue,
#[wasm_bindgen(readonly, skip_typescript)]
pub ir: String,
#[wasm_bindgen(readonly, skip_typescript)]
#[tsify(type = "SymbolTable")]
pub symbols: JsValue,
#[wasm_bindgen(readonly, skip_typescript, js_name = "scopeText")]
pub scope_text: String,
#[wasm_bindgen(readonly, skip_typescript, js_name = "codegenText")]
pub codegen_text: String,
#[wasm_bindgen(readonly, skip_typescript, js_name = "formattedText")]
pub formatted_text: String,
#[wasm_bindgen(readonly, skip_typescript, js_name = "prettierFormattedText")]
pub prettier_formatted_text: String,
#[wasm_bindgen(readonly, skip_typescript, js_name = "prettierIrText")]
pub prettier_ir_text: String,
comments: Vec<Comment>,
diagnostics: RefCell<Vec<Error>>,
#[serde(skip)]
serializer: serde_wasm_bindgen::Serializer,
}
#[derive(Clone, Tsify, Serialize)]
#[tsify(into_wasm_abi)]
pub struct Comment {
pub r#type: CommentType,
pub value: String,
pub start: u32,
pub end: u32,
}
#[derive(Clone, Copy, Tsify, Serialize)]
#[tsify(into_wasm_abi)]
pub enum CommentType {
Line,
Block,
}
#[derive(Default, Clone, Serialize)]
pub struct OxcDiagnostic {
pub start: usize,
pub end: usize,
pub severity: String,
pub message: String,
}
#[wasm_bindgen]
impl Oxc {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
console_error_panic_hook::set_once();
Self { serializer: serde_wasm_bindgen::Serializer::json_compatible(), ..Self::default() }
}
/// Returns Array of String
/// # Errors
/// # Panics
#[wasm_bindgen(js_name = getDiagnostics)]
pub fn get_diagnostics(&self) -> Result<Vec<JsValue>, serde_wasm_bindgen::Error> {
Ok(self
.diagnostics
.borrow()
.iter()
.flat_map(|error| {
let Some(labels) = error.labels() else { return vec![] };
labels
.map(|label| {
OxcDiagnostic {
start: label.offset(),
end: label.offset() + label.len(),
severity: format!("{:?}", error.severity().unwrap_or_default()),
message: format!("{error}"),
}
.serialize(&self.serializer)
.unwrap()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>())
}
/// Returns comments
/// # Errors
#[wasm_bindgen(js_name = getComments)]
pub fn get_comments(&self) -> Result<Vec<JsValue>, serde_wasm_bindgen::Error> {
self.comments.iter().map(|c| c.serialize(&self.serializer)).collect()
}
/// # Errors
/// Serde serialization error
#[wasm_bindgen]
pub fn run(
&mut self,
source_text: &str,
options: OxcOptions,
) -> Result<(), serde_wasm_bindgen::Error> {
self.diagnostics = RefCell::default();
let OxcOptions {
run: run_options,
parser: parser_options,
linter: linter_options,
transformer: transform_options,
codegen: codegen_options,
minifier: minifier_options,
} = options;
let run_options = run_options.unwrap_or_default();
let parser_options = parser_options.unwrap_or_default();
let _linter_options = linter_options.unwrap_or_default();
let minifier_options = minifier_options.unwrap_or_default();
let _codegen_options = codegen_options.unwrap_or_default();
let _transform_options = transform_options.unwrap_or_default();
let allocator = Allocator::default();
let path = PathBuf::from(
parser_options.source_filename.clone().unwrap_or_else(|| "test.tsx".to_string()),
);
let source_type = SourceType::from_path(&path).unwrap_or_default();
let source_type = match parser_options.source_type.as_deref() {
Some("script") => source_type.with_script(true),
Some("module") => source_type.with_module(true),
_ => source_type,
};
let default_parser_options = ParseOptions::default();
let oxc_parser_options = ParseOptions {
parse_regular_expression: true,
allow_return_outside_function: parser_options
.allow_return_outside_function
.unwrap_or(default_parser_options.allow_return_outside_function),
preserve_parens: parser_options
.preserve_parens
.unwrap_or(default_parser_options.preserve_parens),
};
let ParserReturn { mut program, errors, trivias, .. } =
Parser::new(&allocator, source_text, source_type)
.with_options(oxc_parser_options)
.parse();
self.comments = Self::map_comments(source_text, &trivias);
self.ir = format!("{:#?}", program.body);
self.ast = program.serialize(&self.serializer)?;
self.save_diagnostics(errors.into_iter().map(Error::from).collect::<Vec<_>>());
let semantic_ret = SemanticBuilder::new(source_text, source_type)
.with_trivias(trivias.clone())
.with_check_syntax_error(true)
.build_module_record(path.clone(), &program)
.build(&program);
if run_options.syntax.unwrap_or_default() {
self.save_diagnostics(
semantic_ret.errors.into_iter().map(Error::from).collect::<Vec<_>>(),
);
}
self.run_linter(&run_options, source_text, source_type, &path, &trivias, &program);
self.run_prettier(&run_options, source_text, source_type);
let (symbols, scopes) = semantic_ret.semantic.into_symbol_table_and_scope_tree();
if run_options.scope.unwrap_or_default() || run_options.symbol.unwrap_or_default() {
if run_options.scope.unwrap_or_default() {
self.scope_text = Self::get_scope_text(&program, &symbols, &scopes);
}
if run_options.symbol.unwrap_or_default() {
self.symbols = symbols.serialize(&self.serializer)?;
}
}
if run_options.transform.unwrap_or_default() {
let options = TransformOptions::default();
let result = Transformer::new(
&allocator,
&path,
source_type,
source_text,
trivias.clone(),
options,
)
.build_with_symbols_and_scopes(symbols, scopes, &mut program);
if !result.errors.is_empty() {
self.save_diagnostics(
result.errors.into_iter().map(Error::from).collect::<Vec<_>>(),
);
}
}
if minifier_options.compress.unwrap_or_default()
|| minifier_options.mangle.unwrap_or_default()
{
let compress_options = minifier_options.compress_options.unwrap_or_default();
let options = MinifierOptions {
mangle: minifier_options.mangle.unwrap_or_default(),
compress: if minifier_options.compress.unwrap_or_default() {
CompressOptions {
booleans: compress_options.booleans,
drop_console: compress_options.drop_console,
drop_debugger: compress_options.drop_debugger,
evaluate: compress_options.evaluate,
join_vars: compress_options.join_vars,
loops: compress_options.loops,
typeofs: compress_options.typeofs,
..CompressOptions::default()
}
} else {
CompressOptions::all_false()
},
};
Minifier::new(options).build(&allocator, &mut program);
}
self.codegen_text = CodeGenerator::new()
.with_options(CodegenOptions {
minify: minifier_options.whitespace.unwrap_or_default(),
..CodegenOptions::default()
})
.build(&program)
.source_text;
Ok(())
}
fn run_linter(
&mut self,
run_options: &OxcRunOptions,
source_text: &str,
source_type: SourceType,
path: &Path,
trivias: &Trivias,
program: &Program,
) {
// Only lint if there are no syntax errors
if run_options.lint.unwrap_or_default() && self.diagnostics.borrow().is_empty() {
let semantic_ret = SemanticBuilder::new(source_text, source_type)
.with_cfg(true)
.with_trivias(trivias.clone())
.build_module_record(path.to_path_buf(), program)
.build(program);
let semantic = Rc::new(semantic_ret.semantic);
let linter_ret = Linter::default().run(path, Rc::clone(&semantic));
let diagnostics = linter_ret.into_iter().map(|e| Error::from(e.error)).collect();
self.save_diagnostics(diagnostics);
}
}
fn run_prettier(
&mut self,
run_options: &OxcRunOptions,
source_text: &str,
source_type: SourceType,
) {
let allocator = Allocator::default();
if run_options.prettier_format.unwrap_or_default()
|| run_options.prettier_ir.unwrap_or_default()
{
let ret = Parser::new(&allocator, source_text, source_type)
.with_options(ParseOptions { preserve_parens: false, ..ParseOptions::default() })
.parse();
let mut prettier = Prettier::new(
&allocator,
source_text,
ret.trivias.clone(),
PrettierOptions::default(),
);
if run_options.prettier_format.unwrap_or_default() {
self.prettier_formatted_text = prettier.build(&ret.program);
}
if run_options.prettier_ir.unwrap_or_default() {
let prettier_doc = prettier.doc(&ret.program).to_string();
self.prettier_ir_text = {
let ret = Parser::new(&allocator, &prettier_doc, SourceType::default()).parse();
Prettier::new(
&allocator,
&prettier_doc,
ret.trivias,
PrettierOptions::default(),
)
.build(&ret.program)
};
}
}
}
fn get_scope_text(program: &Program<'_>, symbols: &SymbolTable, scopes: &ScopeTree) -> String {
struct ScopesTextWriter<'s> {
symbols: &'s SymbolTable,
scopes: &'s ScopeTree,
scope_text: String,
indent: usize,
space: String,
}
impl<'s> ScopesTextWriter<'s> {
fn new(symbols: &'s SymbolTable, scopes: &'s ScopeTree) -> Self {
Self { symbols, scopes, scope_text: String::new(), indent: 0, space: String::new() }
}
fn write_line<S: AsRef<str>>(&mut self, line: S) {
self.scope_text.push_str(&self.space[0..self.indent]);
self.scope_text.push_str(line.as_ref());
self.scope_text.push('\n');
}
fn indent_in(&mut self) {
self.indent += 2;
if self.space.len() < self.indent {
self.space.push_str(" ");
}
}
fn indent_out(&mut self) {
self.indent -= 2;
}
}
impl<'a, 's> Visit<'a> for ScopesTextWriter<'s> {
fn enter_scope(&mut self, flags: ScopeFlags, scope_id: &Cell<Option<ScopeId>>) {
let scope_id = scope_id.get().unwrap();
self.write_line(format!("Scope {} ({flags:?}) {{", scope_id.index()));
self.indent_in();
let bindings = self.scopes.get_bindings(scope_id);
if !bindings.is_empty() {
self.write_line("Bindings: {");
bindings.iter().for_each(|(name, &symbol_id)| {
let symbol_flags = self.symbols.get_flags(symbol_id);
self.write_line(format!(" {name} ({symbol_id:?} {symbol_flags:?})",));
});
self.write_line("}");
}
}
fn leave_scope(&mut self) {
self.indent_out();
self.write_line("}");
}
}
let mut writer = ScopesTextWriter::new(symbols, scopes);
writer.visit_program(program);
writer.scope_text
}
fn save_diagnostics(&self, diagnostics: Vec<Error>) {
self.diagnostics.borrow_mut().extend(diagnostics);
}
fn map_comments(source_text: &str, trivias: &Trivias) -> Vec<Comment> {
trivias
.comments()
.map(|comment| Comment {
r#type: match comment.kind {
CommentKind::SingleLine => CommentType::Line,
CommentKind::MultiLine => CommentType::Block,
},
value: comment.span.source_text(source_text).to_string(),
start: comment.span.start,
end: comment.span.end,
})
.collect()
}
}