mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 20:28:58 +00:00
feat: oxc transform binding (#3896)
closes #3877 --------- Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
parent
fc48cb4560
commit
d3cd3ea2de
8 changed files with 262 additions and 4 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1772,6 +1772,7 @@ dependencies = [
|
|||
"oxc_isolated_declarations",
|
||||
"oxc_parser",
|
||||
"oxc_span",
|
||||
"oxc_transformer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
encode(self)
|
||||
encode_to_string(self)
|
||||
}
|
||||
|
||||
/// Convert `SourceMap` to vlq sourcemap data url.
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
43
napi/transform/index.d.ts
vendored
43
napi/transform/index.d.ts
vendored
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
mod transformer;
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_codegen::CodeGenerator;
|
||||
use oxc_isolated_declarations::IsolatedDeclarations;
|
||||
|
|
|
|||
186
napi/transform/src/transformer.rs
Normal file
186
napi/transform/src/transformer.rs
Normal file
|
|
@ -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…
Reference in a new issue