Skip to content

feat: oxc transform binding #3896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
}
}
Loading