diff --git a/Cargo.lock b/Cargo.lock index 40a79ef7d191e..4ec9ee77498ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1772,6 +1772,7 @@ dependencies = [ "oxc_isolated_declarations", "oxc_parser", "oxc_span", + "oxc_transformer", ] [[package]] diff --git a/crates/oxc_sourcemap/src/encode.rs b/crates/oxc_sourcemap/src/encode.rs index 66f9dab67739a..f063c0dfb6949 100644 --- a/crates/oxc_sourcemap/src/encode.rs +++ b/crates/oxc_sourcemap/src/encode.rs @@ -2,15 +2,30 @@ use rayon::prelude::*; use crate::error::{Error, Result}; +use crate::JSONSourceMap; /// Port from https://github.com/getsentry/rust-sourcemap/blob/master/src/encoder.rs /// It is a helper for encode `SourceMap` to vlq sourcemap string, but here some different. /// - Quote `source_content` at parallel. /// - If you using `ConcatSourceMapBuilder`, serialize `tokens` to vlq `mappings` at parallel. use crate::{token::TokenChunk, SourceMap, Token}; +pub fn encode(sourcemap: &SourceMap) -> JSONSourceMap { + JSONSourceMap { + file: sourcemap.get_file().map(ToString::to_string), + mappings: Some(serialize_sourcemap_mappings(sourcemap)), + source_root: sourcemap.get_source_root().map(ToString::to_string), + sources: Some(sourcemap.sources.iter().map(ToString::to_string).map(Some).collect()), + sources_content: sourcemap + .source_contents + .as_ref() + .map(|x| x.iter().map(ToString::to_string).map(Some).collect()), + names: Some(sourcemap.names.iter().map(ToString::to_string).collect()), + } +} + // Here using `serde_json::to_string` to serialization `names/source_contents/sources`. // It will escape the string to avoid invalid JSON string. -pub fn encode(sourcemap: &SourceMap) -> Result { +pub fn encode_to_string(sourcemap: &SourceMap) -> Result { let mut buf = String::new(); buf.push_str("{\"version\":3,"); if let Some(file) = sourcemap.get_file() { diff --git a/crates/oxc_sourcemap/src/sourcemap.rs b/crates/oxc_sourcemap/src/sourcemap.rs index 36d9f5241b772..a640b7959917d 100644 --- a/crates/oxc_sourcemap/src/sourcemap.rs +++ b/crates/oxc_sourcemap/src/sourcemap.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{ decode::{decode, decode_from_string, JSONSourceMap}, - encode::encode, + encode::{encode, encode_to_string}, error::Result, token::{Token, TokenChunk}, SourceViewToken, @@ -62,12 +62,20 @@ impl SourceMap { decode_from_string(value) } + /// Convert `SourceMap` to vlq sourcemap. + /// # Errors + /// + /// The `serde_json` serialization Error. + pub fn to_json(&self) -> JSONSourceMap { + encode(self) + } + /// Convert `SourceMap` to vlq sourcemap string. /// # Errors /// /// The `serde_json` serialization Error. pub fn to_json_string(&self) -> Result { - encode(self) + encode_to_string(self) } /// Convert `SourceMap` to vlq sourcemap data url. diff --git a/napi/transform/Cargo.toml b/napi/transform/Cargo.toml index 5783b294a1816..1ed3cf6d190d3 100644 --- a/napi/transform/Cargo.toml +++ b/napi/transform/Cargo.toml @@ -26,6 +26,7 @@ oxc_parser = { workspace = true } oxc_span = { workspace = true } oxc_codegen = { workspace = true } oxc_isolated_declarations = { workspace = true } +oxc_transformer = { workspace = true } napi = { workspace = true } napi-derive = { workspace = true } diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index 9c46ab85a6381..42f04ad75ba0f 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -3,6 +3,49 @@ /* auto-generated by NAPI-RS */ +export interface TypeScriptBindingOptions { + jsxPragma: string + jsxPragmaFrag: string + onlyRemoveTypeImports: boolean + allowNamespaces: boolean + allowDeclareFields: boolean +} +export interface ReactBindingOptions { + runtime: 'classic' | 'automatic' + development: boolean + throwIfNamespace: boolean + pure: boolean + importSource?: string + pragma?: string + pragmaFrag?: string + useBuiltIns?: boolean + useSpread?: boolean +} +export interface ArrowFunctionsBindingOptions { + spec: boolean +} +export interface Es2015BindingOptions { + arrowFunction?: ArrowFunctionsBindingOptions +} +export interface TransformBindingOptions { + typescript: TypeScriptBindingOptions + react: ReactBindingOptions + es2015: Es2015BindingOptions +} +export interface Sourcemap { + file?: string + mappings?: string + sourceRoot?: string + sources?: Array + sourcesContent?: Array + names?: Array +} +export interface TransformResult { + sourceText: string + map?: Sourcemap + errors: Array +} +export function transform(filename: string, sourceText: string, options: TransformBindingOptions): TransformResult export interface IsolatedDeclarationsResult { sourceText: string errors: Array diff --git a/napi/transform/index.js b/napi/transform/index.js index 5cffd10b90028..5220f9bbc7906 100644 --- a/napi/transform/index.js +++ b/napi/transform/index.js @@ -310,6 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { isolatedDeclaration } = nativeBinding +const { transform, isolatedDeclaration } = nativeBinding +module.exports.transform = transform module.exports.isolatedDeclaration = isolatedDeclaration diff --git a/napi/transform/src/lib.rs b/napi/transform/src/lib.rs index 993de6c0ab29b..8f0fb5f910d00 100644 --- a/napi/transform/src/lib.rs +++ b/napi/transform/src/lib.rs @@ -1,4 +1,7 @@ +mod transformer; + use napi_derive::napi; + use oxc_allocator::Allocator; use oxc_codegen::CodeGenerator; use oxc_isolated_declarations::IsolatedDeclarations; diff --git a/napi/transform/src/transformer.rs b/napi/transform/src/transformer.rs new file mode 100644 index 0000000000000..329e90f37ad24 --- /dev/null +++ b/napi/transform/src/transformer.rs @@ -0,0 +1,186 @@ +use std::path::Path; + +use napi_derive::napi; +use oxc_allocator::Allocator; +use oxc_codegen::CodeGenerator; +use oxc_parser::Parser; +use oxc_span::SourceType; +use oxc_transformer::{ + ArrowFunctionsOptions, ES2015Options, ReactJsxRuntime, ReactOptions, TransformOptions, + Transformer, TypeScriptOptions, +}; + +#[napi(object)] +pub struct TypeScriptBindingOptions { + pub jsx_pragma: String, + pub jsx_pragma_frag: String, + pub only_remove_type_imports: bool, + pub allow_namespaces: bool, + pub allow_declare_fields: bool, +} + +impl From for TypeScriptOptions { + fn from(options: TypeScriptBindingOptions) -> Self { + TypeScriptOptions { + jsx_pragma: options.jsx_pragma.into(), + jsx_pragma_frag: options.jsx_pragma_frag.into(), + only_remove_type_imports: options.only_remove_type_imports, + allow_namespaces: options.allow_namespaces, + allow_declare_fields: options.allow_declare_fields, + } + } +} + +#[napi(object)] +pub struct ReactBindingOptions { + #[napi(ts_type = "'classic' | 'automatic'")] + pub runtime: String, + pub development: bool, + pub throw_if_namespace: bool, + pub pure: bool, + pub import_source: Option, + pub pragma: Option, + pub pragma_frag: Option, + pub use_built_ins: Option, + pub use_spread: Option, +} + +impl From for ReactOptions { + fn from(options: ReactBindingOptions) -> Self { + ReactOptions { + runtime: match options.runtime.as_str() { + "classic" => ReactJsxRuntime::Classic, + /* "automatic" */ _ => ReactJsxRuntime::Automatic, + }, + development: options.development, + throw_if_namespace: options.throw_if_namespace, + pure: options.pure, + import_source: options.import_source, + pragma: options.pragma, + pragma_frag: options.pragma_frag, + use_built_ins: options.use_built_ins, + use_spread: options.use_spread, + ..Default::default() + } + } +} + +#[napi(object)] +pub struct ArrowFunctionsBindingOptions { + pub spec: bool, +} + +impl From for ArrowFunctionsOptions { + fn from(options: ArrowFunctionsBindingOptions) -> Self { + ArrowFunctionsOptions { spec: options.spec } + } +} + +#[napi(object)] +pub struct ES2015BindingOptions { + pub arrow_function: Option, +} + +impl From for ES2015Options { + fn from(options: ES2015BindingOptions) -> Self { + ES2015Options { arrow_function: options.arrow_function.map(Into::into) } + } +} + +#[napi(object)] +pub struct TransformBindingOptions { + pub typescript: TypeScriptBindingOptions, + pub react: ReactBindingOptions, + pub es2015: ES2015BindingOptions, + /// Enable Sourcemaps + /// + /// * `true` to generate a sourcemap for the code and include it in the result object. + /// + /// Default: false + pub sourcemaps: bool, +} + +impl From for TransformOptions { + fn from(options: TransformBindingOptions) -> Self { + TransformOptions { + typescript: options.typescript.into(), + react: options.react.into(), + es2015: options.es2015.into(), + ..TransformOptions::default() + } + } +} + +#[napi(object)] +pub struct Sourcemap { + pub file: Option, + pub mappings: Option, + pub source_root: Option, + pub sources: Option>>, + pub sources_content: Option>>, + pub names: Option>, +} + +#[napi(object)] +pub struct TransformResult { + pub source_text: String, + /// Sourcemap + pub map: Option, + pub errors: Vec, +} + +#[allow(clippy::needless_pass_by_value, dead_code)] +#[napi] +pub fn transform( + filename: String, + source_text: String, + options: TransformBindingOptions, +) -> TransformResult { + let sourcemaps = options.sourcemaps; + let mut errors = vec![]; + + let source_path = Path::new(&filename); + let source_type = SourceType::from_path(source_path).unwrap_or_default(); + let allocator = Allocator::default(); + let parser_ret = Parser::new(&allocator, &source_text, source_type).parse(); + if !parser_ret.errors.is_empty() { + errors.extend(parser_ret.errors.into_iter().map(|error| error.message.to_string())); + } + + let mut program = parser_ret.program; + let transform_options = TransformOptions::from(options); + if let Err(e) = Transformer::new( + &allocator, + source_path, + source_type, + &source_text, + parser_ret.trivias.clone(), + transform_options, + ) + .build(&mut program) + { + errors.extend(e.into_iter().map(|error| error.to_string())); + } + + let mut codegen = CodeGenerator::new(); + if sourcemaps { + codegen = codegen.enable_source_map(source_path.to_string_lossy().as_ref(), &source_text); + } + let ret = codegen.build(&program); + + TransformResult { + source_text: ret.source_text, + map: ret.source_map.map(|sourcemap| { + let json = sourcemap.to_json(); + Sourcemap { + file: json.file, + mappings: json.mappings, + source_root: json.source_root, + sources: json.sources, + sources_content: json.sources_content, + names: json.names, + } + }), + errors, + } +}