Skip to content

Commit

Permalink
feat: oxc transform binding (#3896)
Browse files Browse the repository at this point in the history
closes #3877

---------

Co-authored-by: Boshen <boshenc@gmail.com>
  • Loading branch information
underfin and Boshen authored Jun 26, 2024
1 parent fc48cb4 commit d3cd3ea
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion crates/oxc_sourcemap/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
pub fn encode_to_string(sourcemap: &SourceMap) -> Result<String> {
let mut buf = String::new();
buf.push_str("{\"version\":3,");
if let Some(file) = sourcemap.get_file() {
Expand Down
12 changes: 10 additions & 2 deletions crates/oxc_sourcemap/src/sourcemap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<String> {
encode(self)
encode_to_string(self)
}

/// Convert `SourceMap` to vlq sourcemap data url.
Expand Down
1 change: 1 addition & 0 deletions napi/transform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
43 changes: 43 additions & 0 deletions napi/transform/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined | null>
sourcesContent?: Array<string | undefined | null>
names?: Array<string>
}
export interface TransformResult {
sourceText: string
map?: Sourcemap
errors: Array<string>
}
export function transform(filename: string, sourceText: string, options: TransformBindingOptions): TransformResult
export interface IsolatedDeclarationsResult {
sourceText: string
errors: Array<string>
Expand Down
3 changes: 2 additions & 1 deletion napi/transform/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions napi/transform/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
mod transformer;

use napi_derive::napi;

use oxc_allocator::Allocator;
use oxc_codegen::CodeGenerator;
use oxc_isolated_declarations::IsolatedDeclarations;
Expand Down
186 changes: 186 additions & 0 deletions napi/transform/src/transformer.rs
Original file line number Diff line number Diff line change
@@ -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<TypeScriptBindingOptions> 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<String>,
pub pragma: Option<String>,
pub pragma_frag: Option<String>,
pub use_built_ins: Option<bool>,
pub use_spread: Option<bool>,
}

impl From<ReactBindingOptions> 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<ArrowFunctionsBindingOptions> for ArrowFunctionsOptions {
fn from(options: ArrowFunctionsBindingOptions) -> Self {
ArrowFunctionsOptions { spec: options.spec }
}
}

#[napi(object)]
pub struct ES2015BindingOptions {
pub arrow_function: Option<ArrowFunctionsBindingOptions>,
}

impl From<ES2015BindingOptions> 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<TransformBindingOptions> 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<String>,
pub mappings: Option<String>,
pub source_root: Option<String>,
pub sources: Option<Vec<Option<String>>>,
pub sources_content: Option<Vec<Option<String>>>,
pub names: Option<Vec<String>>,
}

#[napi(object)]
pub struct TransformResult {
pub source_text: String,
/// Sourcemap
pub map: Option<Sourcemap>,
pub errors: Vec<String>,
}

#[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,
}
}

0 comments on commit d3cd3ea

Please sign in to comment.