1#![expect(rustdoc::bare_urls)]
3
4use std::{
5 ops::ControlFlow,
6 path::{Path, PathBuf},
7};
8
9use napi::{Either, Task, bindgen_prelude::AsyncTask};
10use napi_derive::napi;
11use rustc_hash::FxHashMap;
12
13use oxc::{
14 CompilerInterface,
15 allocator::Allocator,
16 codegen::{Codegen, CodegenOptions, CodegenReturn},
17 diagnostics::OxcDiagnostic,
18 parser::Parser,
19 semantic::{SemanticBuilder, SemanticBuilderReturn},
20 span::SourceType,
21 transformer::{
22 EnvOptions, HelperLoaderMode, HelperLoaderOptions, JsxRuntime, ProposalOptions,
23 RewriteExtensionsMode,
24 },
25 transformer_plugins::{
26 InjectGlobalVariablesConfig, InjectImport, ModuleRunnerTransform,
27 ReplaceGlobalDefinesConfig,
28 },
29};
30use oxc_napi::{OxcError, get_source_type};
31use oxc_sourcemap::napi::SourceMap;
32
33use crate::IsolatedDeclarationsOptions;
34
35#[derive(Default)]
36#[napi(object)]
37pub struct TransformResult {
38 pub code: String,
42
43 pub map: Option<SourceMap>,
47
48 pub declaration: Option<String>,
57
58 pub declaration_map: Option<SourceMap>,
62
63 #[napi(ts_type = "Record<string, string>")]
73 pub helpers_used: FxHashMap<String, String>,
74
75 pub errors: Vec<OxcError>,
81}
82
83#[napi(object)]
87#[derive(Default)]
88pub struct TransformOptions {
89 #[napi(ts_type = "'js' | 'jsx' | 'ts' | 'tsx' | 'dts'")]
91 pub lang: Option<String>,
92
93 #[napi(ts_type = "'script' | 'module' | 'unambiguous' | undefined")]
95 pub source_type: Option<String>,
96
97 pub cwd: Option<String>,
100
101 pub sourcemap: Option<bool>,
109
110 pub assumptions: Option<CompilerAssumptions>,
112
113 pub typescript: Option<TypeScriptOptions>,
115
116 #[napi(ts_type = "'preserve' | JsxOptions")]
118 pub jsx: Option<Either<String, JsxOptions>>,
119
120 pub target: Option<Either<String, Vec<String>>>,
133
134 pub helpers: Option<Helpers>,
136
137 #[napi(ts_type = "Record<string, string>")]
139 pub define: Option<FxHashMap<String, String>>,
140
141 #[napi(ts_type = "Record<string, string | [string, string]>")]
143 pub inject: Option<FxHashMap<String, Either<String, Vec<String>>>>,
144
145 pub decorator: Option<DecoratorOptions>,
147
148 pub plugins: Option<PluginsOptions>,
150}
151
152impl TryFrom<TransformOptions> for oxc::transformer::TransformOptions {
153 type Error = String;
154
155 fn try_from(options: TransformOptions) -> Result<Self, Self::Error> {
156 let env = match options.target {
157 Some(Either::A(s)) => EnvOptions::from_target(&s)?,
158 Some(Either::B(list)) => EnvOptions::from_target_list(&list)?,
159 _ => EnvOptions::default(),
160 };
161 Ok(Self {
162 cwd: options.cwd.map(PathBuf::from).unwrap_or_default(),
163 assumptions: options.assumptions.map(Into::into).unwrap_or_default(),
164 typescript: options
165 .typescript
166 .map(oxc::transformer::TypeScriptOptions::from)
167 .unwrap_or_default(),
168 decorator: options
169 .decorator
170 .map(oxc::transformer::DecoratorOptions::from)
171 .unwrap_or_default(),
172 jsx: match options.jsx {
173 Some(Either::A(s)) => {
174 if s == "preserve" {
175 oxc::transformer::JsxOptions::disable()
176 } else {
177 return Err(format!("Invalid jsx option: `{s}`."));
178 }
179 }
180 Some(Either::B(options)) => oxc::transformer::JsxOptions::from(options),
181 None => oxc::transformer::JsxOptions::enable(),
182 },
183 env,
184 proposals: ProposalOptions::default(),
185 helper_loader: options
186 .helpers
187 .map_or_else(HelperLoaderOptions::default, HelperLoaderOptions::from),
188 plugins: options
189 .plugins
190 .map(oxc::transformer::PluginsOptions::from)
191 .unwrap_or_default(),
192 })
193 }
194}
195
196#[napi(object)]
197#[derive(Default, Debug)]
198pub struct CompilerAssumptions {
199 pub ignore_function_length: Option<bool>,
200 pub no_document_all: Option<bool>,
201 pub object_rest_no_symbols: Option<bool>,
202 pub pure_getters: Option<bool>,
203 pub set_public_class_fields: Option<bool>,
243}
244
245impl From<CompilerAssumptions> for oxc::transformer::CompilerAssumptions {
246 fn from(value: CompilerAssumptions) -> Self {
247 let ops = oxc::transformer::CompilerAssumptions::default();
248 Self {
249 ignore_function_length: value
250 .ignore_function_length
251 .unwrap_or(ops.ignore_function_length),
252 no_document_all: value.no_document_all.unwrap_or(ops.no_document_all),
253 object_rest_no_symbols: value
254 .object_rest_no_symbols
255 .unwrap_or(ops.object_rest_no_symbols),
256 pure_getters: value.pure_getters.unwrap_or(ops.pure_getters),
257 set_public_class_fields: value
258 .set_public_class_fields
259 .unwrap_or(ops.set_public_class_fields),
260 ..ops
261 }
262 }
263}
264
265#[napi(object)]
266#[derive(Default)]
267pub struct TypeScriptOptions {
268 pub jsx_pragma: Option<String>,
269 pub jsx_pragma_frag: Option<String>,
270 pub only_remove_type_imports: Option<bool>,
271 pub allow_namespaces: Option<bool>,
272 pub allow_declare_fields: Option<bool>,
279 pub remove_class_fields_without_initializer: Option<bool>,
312 pub declaration: Option<IsolatedDeclarationsOptions>,
320 #[napi(ts_type = "'rewrite' | 'remove' | boolean")]
329 pub rewrite_import_extensions: Option<Either<bool, String>>,
330}
331
332impl From<TypeScriptOptions> for oxc::transformer::TypeScriptOptions {
333 fn from(options: TypeScriptOptions) -> Self {
334 let ops = oxc::transformer::TypeScriptOptions::default();
335 oxc::transformer::TypeScriptOptions {
336 jsx_pragma: options.jsx_pragma.map(Into::into).unwrap_or(ops.jsx_pragma),
337 jsx_pragma_frag: options.jsx_pragma_frag.map(Into::into).unwrap_or(ops.jsx_pragma_frag),
338 only_remove_type_imports: options
339 .only_remove_type_imports
340 .unwrap_or(ops.only_remove_type_imports),
341 allow_namespaces: options.allow_namespaces.unwrap_or(ops.allow_namespaces),
342 allow_declare_fields: options.allow_declare_fields.unwrap_or(ops.allow_declare_fields),
343 optimize_const_enums: false,
344 remove_class_fields_without_initializer: options
345 .remove_class_fields_without_initializer
346 .unwrap_or(ops.remove_class_fields_without_initializer),
347 rewrite_import_extensions: options.rewrite_import_extensions.and_then(|value| {
348 match value {
349 Either::A(v) => {
350 if v {
351 Some(RewriteExtensionsMode::Rewrite)
352 } else {
353 None
354 }
355 }
356 Either::B(v) => match v.as_str() {
357 "rewrite" => Some(RewriteExtensionsMode::Rewrite),
358 "remove" => Some(RewriteExtensionsMode::Remove),
359 _ => None,
360 },
361 }
362 }),
363 }
364 }
365}
366
367#[napi(object)]
368#[derive(Default)]
369pub struct DecoratorOptions {
370 pub legacy: Option<bool>,
378
379 pub emit_decorator_metadata: Option<bool>,
387}
388
389impl From<DecoratorOptions> for oxc::transformer::DecoratorOptions {
390 fn from(options: DecoratorOptions) -> Self {
391 oxc::transformer::DecoratorOptions {
392 legacy: options.legacy.unwrap_or_default(),
393 emit_decorator_metadata: options.emit_decorator_metadata.unwrap_or_default(),
394 }
395 }
396}
397
398#[napi(object)]
402#[derive(Default)]
403pub struct StyledComponentsOptions {
404 pub display_name: Option<bool>,
409
410 pub file_name: Option<bool>,
415
416 pub ssr: Option<bool>,
421
422 pub transpile_template_literals: Option<bool>,
427
428 pub minify: Option<bool>,
433
434 pub css_prop: Option<bool>,
440
441 pub pure: Option<bool>,
445
446 pub namespace: Option<String>,
450
451 pub meaningless_file_names: Option<Vec<String>>,
459
460 pub top_level_import_paths: Option<Vec<String>>,
464}
465
466#[napi(object)]
467#[derive(Default)]
468pub struct PluginsOptions {
469 pub styled_components: Option<StyledComponentsOptions>,
470}
471
472impl From<PluginsOptions> for oxc::transformer::PluginsOptions {
473 fn from(options: PluginsOptions) -> Self {
474 oxc::transformer::PluginsOptions {
475 styled_components: options
476 .styled_components
477 .map(oxc::transformer::StyledComponentsOptions::from),
478 }
479 }
480}
481
482impl From<StyledComponentsOptions> for oxc::transformer::StyledComponentsOptions {
483 fn from(options: StyledComponentsOptions) -> Self {
484 let ops = oxc::transformer::StyledComponentsOptions::default();
485 oxc::transformer::StyledComponentsOptions {
486 display_name: options.display_name.unwrap_or(ops.display_name),
487 file_name: options.file_name.unwrap_or(ops.file_name),
488 ssr: options.ssr.unwrap_or(ops.ssr),
489 transpile_template_literals: options
490 .transpile_template_literals
491 .unwrap_or(ops.transpile_template_literals),
492 minify: options.minify.unwrap_or(ops.minify),
493 css_prop: options.css_prop.unwrap_or(ops.css_prop),
494 pure: options.pure.unwrap_or(ops.pure),
495 namespace: options.namespace,
496 meaningless_file_names: options
497 .meaningless_file_names
498 .unwrap_or(ops.meaningless_file_names),
499 top_level_import_paths: options
500 .top_level_import_paths
501 .unwrap_or(ops.top_level_import_paths),
502 }
503 }
504}
505
506#[napi(object)]
510pub struct JsxOptions {
511 #[napi(ts_type = "'classic' | 'automatic'")]
518 pub runtime: Option<String>,
519
520 pub development: Option<bool>,
526
527 pub throw_if_namespace: Option<bool>,
535
536 pub pure: Option<bool>,
544
545 pub import_source: Option<String>,
549
550 pub pragma: Option<String>,
558
559 pub pragma_frag: Option<String>,
566
567 pub use_built_ins: Option<bool>,
573
574 pub use_spread: Option<bool>,
581
582 pub refresh: Option<Either<bool, ReactRefreshOptions>>,
588}
589
590impl From<JsxOptions> for oxc::transformer::JsxOptions {
591 fn from(options: JsxOptions) -> Self {
592 let ops = oxc::transformer::JsxOptions::default();
593 oxc::transformer::JsxOptions {
594 runtime: match options.runtime.as_deref() {
595 Some("classic") => JsxRuntime::Classic,
596 _ => JsxRuntime::Automatic,
597 },
598 development: options.development.unwrap_or(ops.development),
599 throw_if_namespace: options.throw_if_namespace.unwrap_or(ops.throw_if_namespace),
600 pure: options.pure.unwrap_or(ops.pure),
601 import_source: options.import_source,
602 pragma: options.pragma,
603 pragma_frag: options.pragma_frag,
604 use_built_ins: options.use_built_ins,
605 use_spread: options.use_spread,
606 refresh: options.refresh.and_then(|value| match value {
607 Either::A(b) => b.then(oxc::transformer::ReactRefreshOptions::default),
608 Either::B(options) => Some(oxc::transformer::ReactRefreshOptions::from(options)),
609 }),
610 ..Default::default()
611 }
612 }
613}
614
615#[napi(object)]
616pub struct ReactRefreshOptions {
617 pub refresh_reg: Option<String>,
621
622 pub refresh_sig: Option<String>,
626
627 pub emit_full_signatures: Option<bool>,
628}
629
630impl From<ReactRefreshOptions> for oxc::transformer::ReactRefreshOptions {
631 fn from(options: ReactRefreshOptions) -> Self {
632 let ops = oxc::transformer::ReactRefreshOptions::default();
633 oxc::transformer::ReactRefreshOptions {
634 refresh_reg: options.refresh_reg.unwrap_or(ops.refresh_reg),
635 refresh_sig: options.refresh_sig.unwrap_or(ops.refresh_sig),
636 emit_full_signatures: options.emit_full_signatures.unwrap_or(ops.emit_full_signatures),
637 }
638 }
639}
640
641#[napi(object)]
642pub struct ArrowFunctionsOptions {
643 pub spec: Option<bool>,
650}
651
652impl From<ArrowFunctionsOptions> for oxc::transformer::ArrowFunctionsOptions {
653 fn from(options: ArrowFunctionsOptions) -> Self {
654 oxc::transformer::ArrowFunctionsOptions { spec: options.spec.unwrap_or_default() }
655 }
656}
657
658#[napi(object)]
659pub struct Es2015Options {
660 pub arrow_function: Option<ArrowFunctionsOptions>,
662}
663
664impl From<Es2015Options> for oxc::transformer::ES2015Options {
665 fn from(options: Es2015Options) -> Self {
666 oxc::transformer::ES2015Options { arrow_function: options.arrow_function.map(Into::into) }
667 }
668}
669
670#[napi(object)]
671#[derive(Default)]
672pub struct Helpers {
673 pub mode: Option<HelperMode>,
674}
675
676#[derive(Default, Clone, Copy)]
677#[napi(string_enum)]
678pub enum HelperMode {
679 #[default]
688 Runtime,
689 External,
697}
698
699impl From<Helpers> for HelperLoaderOptions {
700 fn from(value: Helpers) -> Self {
701 Self {
702 mode: value.mode.map(HelperLoaderMode::from).unwrap_or_default(),
703 ..HelperLoaderOptions::default()
704 }
705 }
706}
707
708impl From<HelperMode> for HelperLoaderMode {
709 fn from(value: HelperMode) -> Self {
710 match value {
711 HelperMode::Runtime => Self::Runtime,
712 HelperMode::External => Self::External,
713 }
714 }
715}
716
717#[derive(Default)]
718struct Compiler {
719 transform_options: oxc::transformer::TransformOptions,
720 isolated_declaration_options: Option<oxc::isolated_declarations::IsolatedDeclarationsOptions>,
721
722 sourcemap: bool,
723
724 printed: String,
725 printed_sourcemap: Option<SourceMap>,
726
727 declaration: Option<String>,
728 declaration_map: Option<SourceMap>,
729
730 define: Option<ReplaceGlobalDefinesConfig>,
731 inject: Option<InjectGlobalVariablesConfig>,
732
733 helpers_used: FxHashMap<String, String>,
734 errors: Vec<OxcDiagnostic>,
735}
736
737impl Compiler {
738 fn new(options: Option<TransformOptions>) -> Result<Self, Vec<OxcDiagnostic>> {
739 let mut options = options;
740
741 let isolated_declaration_options = options
742 .as_ref()
743 .and_then(|o| o.typescript.as_ref())
744 .and_then(|o| o.declaration)
745 .map(oxc::isolated_declarations::IsolatedDeclarationsOptions::from);
746
747 let sourcemap = options.as_ref().and_then(|o| o.sourcemap).unwrap_or_default();
748
749 let define = options
750 .as_mut()
751 .and_then(|options| options.define.take())
752 .map(|map| {
753 let define = map.into_iter().collect::<Vec<_>>();
754 ReplaceGlobalDefinesConfig::new(&define)
755 })
756 .transpose()?;
757
758 let inject = options
759 .as_mut()
760 .and_then(|options| options.inject.take())
761 .map(|map| {
762 map.into_iter()
763 .map(|(local, value)| match value {
764 Either::A(source) => Ok(InjectImport::default_specifier(&source, &local)),
765 Either::B(v) => {
766 if v.len() != 2 {
767 return Err(vec![OxcDiagnostic::error(
768 "Inject plugin did not receive a tuple [string, string].",
769 )]);
770 }
771 let source = &v[0];
772 Ok(if v[1] == "*" {
773 InjectImport::namespace_specifier(source, &local)
774 } else {
775 InjectImport::named_specifier(source, Some(&v[1]), &local)
776 })
777 }
778 })
779 .collect::<Result<Vec<_>, _>>()
780 })
781 .transpose()?
782 .map(InjectGlobalVariablesConfig::new);
783
784 let transform_options = match options {
785 Some(options) => oxc::transformer::TransformOptions::try_from(options)
786 .map_err(|err| vec![OxcDiagnostic::error(err)])?,
787 None => oxc::transformer::TransformOptions::default(),
788 };
789
790 Ok(Self {
791 transform_options,
792 isolated_declaration_options,
793 sourcemap,
794 printed: String::default(),
795 printed_sourcemap: None,
796 declaration: None,
797 declaration_map: None,
798 define,
799 inject,
800 helpers_used: FxHashMap::default(),
801 errors: vec![],
802 })
803 }
804}
805
806impl CompilerInterface for Compiler {
807 fn handle_errors(&mut self, errors: Vec<OxcDiagnostic>) {
808 self.errors.extend(errors);
809 }
810
811 fn enable_sourcemap(&self) -> bool {
812 self.sourcemap
813 }
814
815 fn transform_options(&self) -> Option<&oxc::transformer::TransformOptions> {
816 Some(&self.transform_options)
817 }
818
819 fn isolated_declaration_options(
820 &self,
821 ) -> Option<oxc::isolated_declarations::IsolatedDeclarationsOptions> {
822 self.isolated_declaration_options
823 }
824
825 fn define_options(&self) -> Option<ReplaceGlobalDefinesConfig> {
826 self.define.clone()
827 }
828
829 fn inject_options(&self) -> Option<InjectGlobalVariablesConfig> {
830 self.inject.clone()
831 }
832
833 fn after_codegen(&mut self, ret: CodegenReturn) {
834 self.printed = ret.code;
835 self.printed_sourcemap = ret.map.map(SourceMap::from);
836 }
837
838 fn after_isolated_declarations(&mut self, ret: CodegenReturn) {
839 self.declaration.replace(ret.code);
840 self.declaration_map = ret.map.map(SourceMap::from);
841 }
842
843 #[expect(deprecated)]
844 fn after_transform(
845 &mut self,
846 _program: &mut oxc::ast::ast::Program<'_>,
847 transformer_return: &mut oxc::transformer::TransformerReturn,
848 ) -> ControlFlow<()> {
849 self.helpers_used = transformer_return
850 .helpers_used
851 .drain()
852 .map(|(helper, source)| (helper.name().to_string(), source))
853 .collect();
854 ControlFlow::Continue(())
855 }
856}
857
858#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
869#[napi]
870pub fn transform(
871 filename: String,
872 source_text: String,
873 options: Option<TransformOptions>,
874) -> TransformResult {
875 let source_path = Path::new(&filename);
876
877 let source_type = get_source_type(
878 &filename,
879 options.as_ref().and_then(|options| options.lang.as_deref()),
880 options.as_ref().and_then(|options| options.source_type.as_deref()),
881 );
882
883 let mut compiler = match Compiler::new(options) {
884 Ok(compiler) => compiler,
885 Err(errors) => {
886 return TransformResult {
887 errors: OxcError::from_diagnostics(&filename, &source_text, errors),
888 ..Default::default()
889 };
890 }
891 };
892
893 compiler.compile(&source_text, source_type, source_path);
894
895 TransformResult {
896 code: compiler.printed,
897 map: compiler.printed_sourcemap,
898 declaration: compiler.declaration,
899 declaration_map: compiler.declaration_map,
900 helpers_used: compiler.helpers_used,
901 errors: OxcError::from_diagnostics(&filename, &source_text, compiler.errors),
902 }
903}
904
905pub struct TransformTask {
906 filename: String,
907 source_text: String,
908 options: Option<TransformOptions>,
909}
910
911#[napi]
912impl Task for TransformTask {
913 type JsValue = TransformResult;
914 type Output = TransformResult;
915
916 fn compute(&mut self) -> napi::Result<Self::Output> {
917 let source_path = Path::new(&self.filename);
918
919 let source_type = get_source_type(
920 &self.filename,
921 self.options.as_ref().and_then(|options| options.lang.as_deref()),
922 self.options.as_ref().and_then(|options| options.source_type.as_deref()),
923 );
924
925 let mut compiler = match Compiler::new(self.options.take()) {
926 Ok(compiler) => compiler,
927 Err(errors) => {
928 return Ok(TransformResult {
929 errors: OxcError::from_diagnostics(&self.filename, &self.source_text, errors),
930 ..Default::default()
931 });
932 }
933 };
934
935 compiler.compile(&self.source_text, source_type, source_path);
936
937 Ok(TransformResult {
938 code: compiler.printed,
939 map: compiler.printed_sourcemap,
940 declaration: compiler.declaration,
941 declaration_map: compiler.declaration_map,
942 helpers_used: compiler.helpers_used,
943 errors: OxcError::from_diagnostics(&self.filename, &self.source_text, compiler.errors),
944 })
945 }
946
947 fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result<Self::JsValue> {
948 Ok(result)
949 }
950}
951
952#[napi]
965pub fn transform_async(
966 filename: String,
967 source_text: String,
968 options: Option<TransformOptions>,
969) -> AsyncTask<TransformTask> {
970 AsyncTask::new(TransformTask { filename, source_text, options })
971}
972
973#[derive(Default)]
974#[napi(object)]
975pub struct ModuleRunnerTransformOptions {
976 pub sourcemap: Option<bool>,
984}
985
986#[derive(Default)]
987#[napi(object)]
988pub struct ModuleRunnerTransformResult {
989 pub code: String,
993
994 pub map: Option<SourceMap>,
998
999 pub deps: Vec<String>,
1001
1002 pub dynamic_deps: Vec<String>,
1004
1005 pub errors: Vec<OxcError>,
1011}
1012
1013#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
1025#[napi]
1026pub fn module_runner_transform(
1027 filename: String,
1028 source_text: String,
1029 options: Option<ModuleRunnerTransformOptions>,
1030) -> ModuleRunnerTransformResult {
1031 let file_path = Path::new(&filename);
1032 let source_type = SourceType::from_path(file_path);
1033 let source_type = match source_type {
1034 Ok(s) => s,
1035 Err(err) => {
1036 return ModuleRunnerTransformResult {
1037 code: String::default(),
1038 map: None,
1039 deps: vec![],
1040 dynamic_deps: vec![],
1041 errors: vec![OxcError::new(err.to_string())],
1042 };
1043 }
1044 };
1045
1046 let allocator = Allocator::default();
1047 let mut parser_ret = Parser::new(&allocator, &source_text, source_type).parse();
1048 let mut program = parser_ret.program;
1049
1050 let SemanticBuilderReturn { semantic, errors } =
1051 SemanticBuilder::new().with_check_syntax_error(true).build(&program);
1052 parser_ret.errors.extend(errors);
1053
1054 let scoping = semantic.into_scoping();
1055 let (deps, dynamic_deps) =
1056 ModuleRunnerTransform::default().transform(&allocator, &mut program, scoping);
1057
1058 let CodegenReturn { code, map, .. } = Codegen::new()
1059 .with_options(CodegenOptions {
1060 source_map_path: options.and_then(|opts| {
1061 opts.sourcemap.as_ref().and_then(|s| s.then(|| file_path.to_path_buf()))
1062 }),
1063 ..Default::default()
1064 })
1065 .build(&program);
1066
1067 ModuleRunnerTransformResult {
1068 code,
1069 map: map.map(Into::into),
1070 deps: deps.into_iter().collect::<Vec<String>>(),
1071 dynamic_deps: dynamic_deps.into_iter().collect::<Vec<String>>(),
1072 errors: OxcError::from_diagnostics(&filename, &source_text, parser_ret.errors),
1073 }
1074}