Skip to content

Commit d3cd3ea

Browse files
underfinBoshen
andauthored
feat: oxc transform binding (#3896)
closes #3877 --------- Co-authored-by: Boshen <boshenc@gmail.com>
1 parent fc48cb4 commit d3cd3ea

8 files changed

Lines changed: 262 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_sourcemap/src/encode.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,30 @@
22
use rayon::prelude::*;
33

44
use crate::error::{Error, Result};
5+
use crate::JSONSourceMap;
56
/// Port from https://github.com/getsentry/rust-sourcemap/blob/master/src/encoder.rs
67
/// It is a helper for encode `SourceMap` to vlq sourcemap string, but here some different.
78
/// - Quote `source_content` at parallel.
89
/// - If you using `ConcatSourceMapBuilder`, serialize `tokens` to vlq `mappings` at parallel.
910
use crate::{token::TokenChunk, SourceMap, Token};
1011

12+
pub fn encode(sourcemap: &SourceMap) -> JSONSourceMap {
13+
JSONSourceMap {
14+
file: sourcemap.get_file().map(ToString::to_string),
15+
mappings: Some(serialize_sourcemap_mappings(sourcemap)),
16+
source_root: sourcemap.get_source_root().map(ToString::to_string),
17+
sources: Some(sourcemap.sources.iter().map(ToString::to_string).map(Some).collect()),
18+
sources_content: sourcemap
19+
.source_contents
20+
.as_ref()
21+
.map(|x| x.iter().map(ToString::to_string).map(Some).collect()),
22+
names: Some(sourcemap.names.iter().map(ToString::to_string).collect()),
23+
}
24+
}
25+
1126
// Here using `serde_json::to_string` to serialization `names/source_contents/sources`.
1227
// It will escape the string to avoid invalid JSON string.
13-
pub fn encode(sourcemap: &SourceMap) -> Result<String> {
28+
pub fn encode_to_string(sourcemap: &SourceMap) -> Result<String> {
1429
let mut buf = String::new();
1530
buf.push_str("{\"version\":3,");
1631
if let Some(file) = sourcemap.get_file() {

crates/oxc_sourcemap/src/sourcemap.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::sync::Arc;
22

33
use crate::{
44
decode::{decode, decode_from_string, JSONSourceMap},
5-
encode::encode,
5+
encode::{encode, encode_to_string},
66
error::Result,
77
token::{Token, TokenChunk},
88
SourceViewToken,
@@ -62,12 +62,20 @@ impl SourceMap {
6262
decode_from_string(value)
6363
}
6464

65+
/// Convert `SourceMap` to vlq sourcemap.
66+
/// # Errors
67+
///
68+
/// The `serde_json` serialization Error.
69+
pub fn to_json(&self) -> JSONSourceMap {
70+
encode(self)
71+
}
72+
6573
/// Convert `SourceMap` to vlq sourcemap string.
6674
/// # Errors
6775
///
6876
/// The `serde_json` serialization Error.
6977
pub fn to_json_string(&self) -> Result<String> {
70-
encode(self)
78+
encode_to_string(self)
7179
}
7280

7381
/// Convert `SourceMap` to vlq sourcemap data url.

napi/transform/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ oxc_parser = { workspace = true }
2626
oxc_span = { workspace = true }
2727
oxc_codegen = { workspace = true }
2828
oxc_isolated_declarations = { workspace = true }
29+
oxc_transformer = { workspace = true }
2930

3031
napi = { workspace = true }
3132
napi-derive = { workspace = true }

napi/transform/index.d.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,49 @@
33

44
/* auto-generated by NAPI-RS */
55

6+
export interface TypeScriptBindingOptions {
7+
jsxPragma: string
8+
jsxPragmaFrag: string
9+
onlyRemoveTypeImports: boolean
10+
allowNamespaces: boolean
11+
allowDeclareFields: boolean
12+
}
13+
export interface ReactBindingOptions {
14+
runtime: 'classic' | 'automatic'
15+
development: boolean
16+
throwIfNamespace: boolean
17+
pure: boolean
18+
importSource?: string
19+
pragma?: string
20+
pragmaFrag?: string
21+
useBuiltIns?: boolean
22+
useSpread?: boolean
23+
}
24+
export interface ArrowFunctionsBindingOptions {
25+
spec: boolean
26+
}
27+
export interface Es2015BindingOptions {
28+
arrowFunction?: ArrowFunctionsBindingOptions
29+
}
30+
export interface TransformBindingOptions {
31+
typescript: TypeScriptBindingOptions
32+
react: ReactBindingOptions
33+
es2015: Es2015BindingOptions
34+
}
35+
export interface Sourcemap {
36+
file?: string
37+
mappings?: string
38+
sourceRoot?: string
39+
sources?: Array<string | undefined | null>
40+
sourcesContent?: Array<string | undefined | null>
41+
names?: Array<string>
42+
}
43+
export interface TransformResult {
44+
sourceText: string
45+
map?: Sourcemap
46+
errors: Array<string>
47+
}
48+
export function transform(filename: string, sourceText: string, options: TransformBindingOptions): TransformResult
649
export interface IsolatedDeclarationsResult {
750
sourceText: string
851
errors: Array<string>

napi/transform/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ if (!nativeBinding) {
310310
throw new Error(`Failed to load native binding`)
311311
}
312312

313-
const { isolatedDeclaration } = nativeBinding
313+
const { transform, isolatedDeclaration } = nativeBinding
314314

315+
module.exports.transform = transform
315316
module.exports.isolatedDeclaration = isolatedDeclaration

napi/transform/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
mod transformer;
2+
13
use napi_derive::napi;
4+
25
use oxc_allocator::Allocator;
36
use oxc_codegen::CodeGenerator;
47
use oxc_isolated_declarations::IsolatedDeclarations;

napi/transform/src/transformer.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use std::path::Path;
2+
3+
use napi_derive::napi;
4+
use oxc_allocator::Allocator;
5+
use oxc_codegen::CodeGenerator;
6+
use oxc_parser::Parser;
7+
use oxc_span::SourceType;
8+
use oxc_transformer::{
9+
ArrowFunctionsOptions, ES2015Options, ReactJsxRuntime, ReactOptions, TransformOptions,
10+
Transformer, TypeScriptOptions,
11+
};
12+
13+
#[napi(object)]
14+
pub struct TypeScriptBindingOptions {
15+
pub jsx_pragma: String,
16+
pub jsx_pragma_frag: String,
17+
pub only_remove_type_imports: bool,
18+
pub allow_namespaces: bool,
19+
pub allow_declare_fields: bool,
20+
}
21+
22+
impl From<TypeScriptBindingOptions> for TypeScriptOptions {
23+
fn from(options: TypeScriptBindingOptions) -> Self {
24+
TypeScriptOptions {
25+
jsx_pragma: options.jsx_pragma.into(),
26+
jsx_pragma_frag: options.jsx_pragma_frag.into(),
27+
only_remove_type_imports: options.only_remove_type_imports,
28+
allow_namespaces: options.allow_namespaces,
29+
allow_declare_fields: options.allow_declare_fields,
30+
}
31+
}
32+
}
33+
34+
#[napi(object)]
35+
pub struct ReactBindingOptions {
36+
#[napi(ts_type = "'classic' | 'automatic'")]
37+
pub runtime: String,
38+
pub development: bool,
39+
pub throw_if_namespace: bool,
40+
pub pure: bool,
41+
pub import_source: Option<String>,
42+
pub pragma: Option<String>,
43+
pub pragma_frag: Option<String>,
44+
pub use_built_ins: Option<bool>,
45+
pub use_spread: Option<bool>,
46+
}
47+
48+
impl From<ReactBindingOptions> for ReactOptions {
49+
fn from(options: ReactBindingOptions) -> Self {
50+
ReactOptions {
51+
runtime: match options.runtime.as_str() {
52+
"classic" => ReactJsxRuntime::Classic,
53+
/* "automatic" */ _ => ReactJsxRuntime::Automatic,
54+
},
55+
development: options.development,
56+
throw_if_namespace: options.throw_if_namespace,
57+
pure: options.pure,
58+
import_source: options.import_source,
59+
pragma: options.pragma,
60+
pragma_frag: options.pragma_frag,
61+
use_built_ins: options.use_built_ins,
62+
use_spread: options.use_spread,
63+
..Default::default()
64+
}
65+
}
66+
}
67+
68+
#[napi(object)]
69+
pub struct ArrowFunctionsBindingOptions {
70+
pub spec: bool,
71+
}
72+
73+
impl From<ArrowFunctionsBindingOptions> for ArrowFunctionsOptions {
74+
fn from(options: ArrowFunctionsBindingOptions) -> Self {
75+
ArrowFunctionsOptions { spec: options.spec }
76+
}
77+
}
78+
79+
#[napi(object)]
80+
pub struct ES2015BindingOptions {
81+
pub arrow_function: Option<ArrowFunctionsBindingOptions>,
82+
}
83+
84+
impl From<ES2015BindingOptions> for ES2015Options {
85+
fn from(options: ES2015BindingOptions) -> Self {
86+
ES2015Options { arrow_function: options.arrow_function.map(Into::into) }
87+
}
88+
}
89+
90+
#[napi(object)]
91+
pub struct TransformBindingOptions {
92+
pub typescript: TypeScriptBindingOptions,
93+
pub react: ReactBindingOptions,
94+
pub es2015: ES2015BindingOptions,
95+
/// Enable Sourcemaps
96+
///
97+
/// * `true` to generate a sourcemap for the code and include it in the result object.
98+
///
99+
/// Default: false
100+
pub sourcemaps: bool,
101+
}
102+
103+
impl From<TransformBindingOptions> for TransformOptions {
104+
fn from(options: TransformBindingOptions) -> Self {
105+
TransformOptions {
106+
typescript: options.typescript.into(),
107+
react: options.react.into(),
108+
es2015: options.es2015.into(),
109+
..TransformOptions::default()
110+
}
111+
}
112+
}
113+
114+
#[napi(object)]
115+
pub struct Sourcemap {
116+
pub file: Option<String>,
117+
pub mappings: Option<String>,
118+
pub source_root: Option<String>,
119+
pub sources: Option<Vec<Option<String>>>,
120+
pub sources_content: Option<Vec<Option<String>>>,
121+
pub names: Option<Vec<String>>,
122+
}
123+
124+
#[napi(object)]
125+
pub struct TransformResult {
126+
pub source_text: String,
127+
/// Sourcemap
128+
pub map: Option<Sourcemap>,
129+
pub errors: Vec<String>,
130+
}
131+
132+
#[allow(clippy::needless_pass_by_value, dead_code)]
133+
#[napi]
134+
pub fn transform(
135+
filename: String,
136+
source_text: String,
137+
options: TransformBindingOptions,
138+
) -> TransformResult {
139+
let sourcemaps = options.sourcemaps;
140+
let mut errors = vec![];
141+
142+
let source_path = Path::new(&filename);
143+
let source_type = SourceType::from_path(source_path).unwrap_or_default();
144+
let allocator = Allocator::default();
145+
let parser_ret = Parser::new(&allocator, &source_text, source_type).parse();
146+
if !parser_ret.errors.is_empty() {
147+
errors.extend(parser_ret.errors.into_iter().map(|error| error.message.to_string()));
148+
}
149+
150+
let mut program = parser_ret.program;
151+
let transform_options = TransformOptions::from(options);
152+
if let Err(e) = Transformer::new(
153+
&allocator,
154+
source_path,
155+
source_type,
156+
&source_text,
157+
parser_ret.trivias.clone(),
158+
transform_options,
159+
)
160+
.build(&mut program)
161+
{
162+
errors.extend(e.into_iter().map(|error| error.to_string()));
163+
}
164+
165+
let mut codegen = CodeGenerator::new();
166+
if sourcemaps {
167+
codegen = codegen.enable_source_map(source_path.to_string_lossy().as_ref(), &source_text);
168+
}
169+
let ret = codegen.build(&program);
170+
171+
TransformResult {
172+
source_text: ret.source_text,
173+
map: ret.source_map.map(|sourcemap| {
174+
let json = sourcemap.to_json();
175+
Sourcemap {
176+
file: json.file,
177+
mappings: json.mappings,
178+
source_root: json.source_root,
179+
sources: json.sources,
180+
sources_content: json.sources_content,
181+
names: json.names,
182+
}
183+
}),
184+
errors,
185+
}
186+
}

0 commit comments

Comments
 (0)