diff --git a/crates/oxc_transformer/src/options/babel/mod.rs b/crates/oxc_transformer/src/options/babel/mod.rs index c782a362fbdc7..4091a16996560 100644 --- a/crates/oxc_transformer/src/options/babel/mod.rs +++ b/crates/oxc_transformer/src/options/babel/mod.rs @@ -1,17 +1,16 @@ mod env; +mod plugins; +mod presets; use std::path::{Path, PathBuf}; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; -use crate::{ - es2015::ArrowFunctionsOptions, es2018::ObjectRestSpreadOptions, es2022::ClassPropertiesOptions, - jsx::JsxOptions, TypeScriptOptions, -}; - pub use env::{BabelEnvOptions, Targets}; +use self::{plugins::BabelPlugins, presets::BabelPresets}; + /// Babel options /// /// @@ -28,7 +27,7 @@ pub struct BabelOptions { pub plugins: BabelPlugins, #[serde(default)] - pub presets: Vec, // Can be a string or an array + pub presets: BabelPresets, // Misc options pub source_type: Option, @@ -59,6 +58,38 @@ pub struct BabelOptions { pub external_helpers: bool, } +/// +#[derive(Debug, Deserialize)] +struct PluginPresetEntries(Vec); + +/// +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum PluginPresetEntry { + String(String), + Vec1([String; 1]), + Tuple(String, serde_json::Value), + Triple(String, serde_json::Value, #[allow(unused)] String), +} + +impl PluginPresetEntry { + fn name(&self) -> &str { + match self { + Self::String(s) | Self::Tuple(s, _) | Self::Triple(s, _, _) => s, + Self::Vec1(s) => &s[0], + } + } + + fn value(self) -> Result { + match self { + Self::String(_) | Self::Vec1(_) => Ok(T::default()), + Self::Tuple(name, v) | Self::Triple(name, v, _) => { + serde_json::from_value::(v).map_err(|err| format!("{name}: {err}")) + } + } + } +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "lowercase")] pub enum TestOs { @@ -84,6 +115,7 @@ impl BabelOptions { pub fn from_test_path(path: &Path) -> Self { let mut babel_options: Option = None; let mut plugins_json = None; + let mut presets_json = None; for path in path.ancestors().take(3) { let file = path.join("options.json"); @@ -99,6 +131,11 @@ impl BabelOptions { plugins_json = new_plugins; } + let new_presets = new_value.as_object_mut().unwrap().remove("presets"); + if presets_json.is_none() { + presets_json = new_presets; + } + let new_options: Self = serde_json::from_value::(new_value) .unwrap_or_else(|err| panic!("{err:?}\n{file:?}\n{content}")); @@ -113,9 +150,6 @@ impl BabelOptions { existing_options.throws = Some(throws); } } - if existing_options.presets.is_empty() { - existing_options.presets = new_options.presets; - } } else { babel_options = Some(new_options); } @@ -123,7 +157,12 @@ impl BabelOptions { let mut options = babel_options.unwrap_or_default(); if let Some(plugins_json) = plugins_json { - options.plugins = serde_json::from_value::(plugins_json).unwrap(); + options.plugins = serde_json::from_value::(plugins_json) + .unwrap_or_else(|err| panic!("{err:?}\n{path:?}")); + } + if let Some(presets_json) = presets_json { + options.presets = serde_json::from_value::(presets_json) + .unwrap_or_else(|err| panic!("{err:?}\n{path:?}")); } options } @@ -147,187 +186,4 @@ impl BabelOptions { pub fn is_unambiguous(&self) -> bool { self.source_type.as_ref().map_or(false, |s| s.as_str() == "unambiguous") } - - pub fn get_preset(&self, name: &str) -> Option> { - self.presets.iter().find_map(|v| Self::get_value(v, name)) - } - - pub fn has_preset(&self, name: &str) -> bool { - self.get_preset(name).is_some() - } - - #[allow(clippy::option_option)] - fn get_value(value: &Value, name: &str) -> Option> { - match value { - Value::String(s) if s == name => Some(None), - Value::Array(a) if a.first().and_then(Value::as_str).is_some_and(|s| s == name) => { - Some(a.get(1).cloned()) - } - _ => None, - } - } -} - -#[derive(Debug, Default, Clone, Copy, Deserialize)] -pub struct SyntaxTypeScriptOptions { - #[serde(default)] - pub dts: bool, -} - -#[derive(Debug, Default, Clone, Deserialize)] -pub struct SyntaxDecoratorOptions { - #[serde(default)] - pub version: String, -} - -#[derive(Debug, Default, Clone, Deserialize)] -#[serde(try_from = "PluginPresetEntries")] -pub struct BabelPlugins { - pub errors: Vec, - pub unsupported: Vec, - // syntax - pub syntax_typescript: Option, - pub syntax_jsx: bool, - // decorators - pub syntax_decorators: Option, - pub proposal_decorators: Option, - // ts - pub typescript: Option, - // jsx - pub react_jsx: Option, - pub react_jsx_dev: Option, - pub react_jsx_self: bool, - pub react_jsx_source: bool, - pub react_display_name: bool, - // regexp - pub sticky_flag: bool, - pub unicode_flag: bool, - pub dot_all_flag: bool, - pub look_behind_assertions: bool, - pub named_capture_groups: bool, - pub unicode_property_escapes: bool, - pub match_indices: bool, - /// Enables plugin to transform the RegExp literal has `v` flag - pub set_notation: bool, - // ES2015 - pub arrow_function: Option, - // ES2016 - pub exponentiation_operator: bool, - // ES2017 - pub async_to_generator: bool, - // ES2018 - pub object_rest_spread: Option, - pub async_generator_functions: bool, - // ES2019 - pub optional_catch_binding: bool, - // ES2020 - pub nullish_coalescing_operator: bool, - // ES2021 - pub logical_assignment_operators: bool, - // ES2022 - pub class_static_block: bool, - pub class_properties: Option, -} - -/// -#[derive(Debug, Deserialize)] -struct PluginPresetEntries(Vec); - -impl TryFrom for BabelPlugins { - type Error = String; - - fn try_from(entries: PluginPresetEntries) -> Result { - let mut p = BabelPlugins::default(); - for entry in entries.0 { - match entry.name() { - "typescript" | "syntax-typescript" => { - p.syntax_typescript = Some(entry.value::()?); - } - "jsx" | "syntax-jsx" => p.syntax_jsx = true, - "syntax-decorators" => { - p.syntax_decorators = Some(entry.value::()?); - } - "proposal-decorators" => { - p.proposal_decorators = Some(entry.value::()?); - } - "transform-typescript" => { - p.typescript = - entry.value::().map_err(|err| p.errors.push(err)).ok(); - } - "transform-react-jsx" => { - p.react_jsx = - entry.value::().map_err(|err| p.errors.push(err)).ok(); - } - "transform-react-jsx-development" => { - p.react_jsx_dev = - entry.value::().map_err(|err| p.errors.push(err)).ok(); - } - "transform-react-display-name" => p.react_display_name = true, - "transform-react-jsx-self" => p.react_jsx_self = true, - "transform-react-jsx-source" => p.react_jsx_source = true, - "transform-sticky-regex" => p.sticky_flag = true, - "transform-unicode-regex" => p.unicode_flag = true, - "transform-dotall-regex" => p.dot_all_flag = true, - "esbuild-regexp-lookbehind-assertions" => p.look_behind_assertions = true, - "transform-named-capturing-groups-regex" => p.named_capture_groups = true, - "transform-unicode-property-regex" => p.unicode_property_escapes = true, - "esbuild-regexp-match-indices" => p.match_indices = true, - "transform-unicode-sets-regex" => p.set_notation = true, - "transform-arrow-functions" => { - p.arrow_function = entry - .value::() - .map_err(|err| p.errors.push(err)) - .ok(); - } - "transform-exponentiation-operator" => p.exponentiation_operator = true, - "transform-async-to-generator" => p.async_to_generator = true, - "transform-object-rest-spread" => { - p.object_rest_spread = entry - .value::() - .inspect_err(|err| p.errors.push(err.to_string())) - .ok(); - } - "transform-async-generator-functions" => p.async_generator_functions = true, - "transform-optional-catch-binding" => p.optional_catch_binding = true, - "transform-nullish-coalescing-operator" => p.nullish_coalescing_operator = true, - "transform-logical-assignment-operators" => p.logical_assignment_operators = true, - "transform-class-static-block" => p.class_static_block = true, - "transform-class-properties" => { - p.class_properties = entry - .value::() - .inspect_err(|err| p.errors.push(err.to_string())) - .ok(); - } - s => p.unsupported.push(s.to_string()), - } - } - Ok(p) - } -} - -/// -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum PluginPresetEntry { - String(String), - Vec1([String; 1]), - Tuple(String, serde_json::Value), -} - -impl PluginPresetEntry { - fn name(&self) -> &str { - match self { - Self::String(s) | Self::Tuple(s, _) => s, - Self::Vec1(s) => &s[0], - } - } - - fn value(self) -> Result { - match self { - Self::String(_) | Self::Vec1(_) => Ok(T::default()), - Self::Tuple(name, v) => { - serde_json::from_value::(v).map_err(|err| format!("{name}: {err}")) - } - } - } } diff --git a/crates/oxc_transformer/src/options/babel/plugins.rs b/crates/oxc_transformer/src/options/babel/plugins.rs new file mode 100644 index 0000000000000..4ff9299356c6d --- /dev/null +++ b/crates/oxc_transformer/src/options/babel/plugins.rs @@ -0,0 +1,141 @@ +use serde::Deserialize; + +use crate::{ + es2015::ArrowFunctionsOptions, es2018::ObjectRestSpreadOptions, es2022::ClassPropertiesOptions, + jsx::JsxOptions, TypeScriptOptions, +}; + +use super::PluginPresetEntries; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +pub struct SyntaxTypeScriptOptions { + #[serde(default)] + pub dts: bool, +} + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct SyntaxDecoratorOptions { + #[serde(default)] + pub version: String, +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(try_from = "PluginPresetEntries")] +pub struct BabelPlugins { + pub errors: Vec, + pub unsupported: Vec, + // syntax + pub syntax_typescript: Option, + pub syntax_jsx: bool, + // decorators + pub syntax_decorators: Option, + pub proposal_decorators: Option, + // ts + pub typescript: Option, + // jsx + pub react_jsx: Option, + pub react_jsx_dev: Option, + pub react_jsx_self: bool, + pub react_jsx_source: bool, + pub react_display_name: bool, + // regexp + pub sticky_flag: bool, + pub unicode_flag: bool, + pub dot_all_flag: bool, + pub look_behind_assertions: bool, + pub named_capture_groups: bool, + pub unicode_property_escapes: bool, + pub match_indices: bool, + /// Enables plugin to transform the RegExp literal has `v` flag + pub set_notation: bool, + // ES2015 + pub arrow_function: Option, + // ES2016 + pub exponentiation_operator: bool, + // ES2017 + pub async_to_generator: bool, + // ES2018 + pub object_rest_spread: Option, + pub async_generator_functions: bool, + // ES2019 + pub optional_catch_binding: bool, + // ES2020 + pub nullish_coalescing_operator: bool, + // ES2021 + pub logical_assignment_operators: bool, + // ES2022 + pub class_static_block: bool, + pub class_properties: Option, +} + +impl TryFrom for BabelPlugins { + type Error = String; + + fn try_from(entries: PluginPresetEntries) -> Result { + let mut p = Self::default(); + for entry in entries.0 { + match entry.name() { + "typescript" | "syntax-typescript" => { + p.syntax_typescript = Some(entry.value::()?); + } + "jsx" | "syntax-jsx" => p.syntax_jsx = true, + "syntax-decorators" => { + p.syntax_decorators = Some(entry.value::()?); + } + "proposal-decorators" => { + p.proposal_decorators = Some(entry.value::()?); + } + "transform-typescript" => { + p.typescript = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "transform-react-jsx" => { + p.react_jsx = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "transform-react-jsx-development" => { + p.react_jsx_dev = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "transform-react-display-name" => p.react_display_name = true, + "transform-react-jsx-self" => p.react_jsx_self = true, + "transform-react-jsx-source" => p.react_jsx_source = true, + "transform-sticky-regex" => p.sticky_flag = true, + "transform-unicode-regex" => p.unicode_flag = true, + "transform-dotall-regex" => p.dot_all_flag = true, + "esbuild-regexp-lookbehind-assertions" => p.look_behind_assertions = true, + "transform-named-capturing-groups-regex" => p.named_capture_groups = true, + "transform-unicode-property-regex" => p.unicode_property_escapes = true, + "esbuild-regexp-match-indices" => p.match_indices = true, + "transform-unicode-sets-regex" => p.set_notation = true, + "transform-arrow-functions" => { + p.arrow_function = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + "transform-exponentiation-operator" => p.exponentiation_operator = true, + "transform-async-to-generator" => p.async_to_generator = true, + "transform-object-rest-spread" => { + p.object_rest_spread = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + "transform-async-generator-functions" => p.async_generator_functions = true, + "transform-optional-catch-binding" => p.optional_catch_binding = true, + "transform-nullish-coalescing-operator" => p.nullish_coalescing_operator = true, + "transform-logical-assignment-operators" => p.logical_assignment_operators = true, + "transform-class-static-block" => p.class_static_block = true, + "transform-class-properties" => { + p.class_properties = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } + s => p.unsupported.push(s.to_string()), + } + } + Ok(p) + } +} diff --git a/crates/oxc_transformer/src/options/babel/presets.rs b/crates/oxc_transformer/src/options/babel/presets.rs new file mode 100644 index 0000000000000..4386e52a7b954 --- /dev/null +++ b/crates/oxc_transformer/src/options/babel/presets.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; + +use super::{BabelEnvOptions, PluginPresetEntries}; + +use crate::{JsxOptions, TypeScriptOptions}; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(try_from = "PluginPresetEntries")] +pub struct BabelPresets { + pub errors: Vec, + pub unsupported: Vec, + + pub env: Option, + + pub jsx: Option, + + pub typescript: Option, +} + +impl TryFrom for BabelPresets { + type Error = String; + + fn try_from(entries: PluginPresetEntries) -> Result { + let mut p = Self::default(); + for entry in entries.0 { + match entry.name() { + "env" => { + p.env = entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "typescript" => { + p.typescript = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + "react" => { + p.jsx = entry.value::().map_err(|err| p.errors.push(err)).ok(); + } + s => p.unsupported.push(s.to_string()), + } + } + Ok(p) + } +} diff --git a/crates/oxc_transformer/src/options/env.rs b/crates/oxc_transformer/src/options/env.rs index 9fecaee2c7fcd..e082e9867b5cd 100644 --- a/crates/oxc_transformer/src/options/env.rs +++ b/crates/oxc_transformer/src/options/env.rs @@ -1,4 +1,4 @@ -use oxc_diagnostics::{Error, OxcDiagnostic}; +use oxc_diagnostics::Error; use crate::{ es2015::ES2015Options, es2016::ES2016Options, es2017::ES2017Options, es2018::ES2018Options, @@ -126,17 +126,11 @@ impl TryFrom<&BabelOptions> for EnvOptions { /// If the `options` contains any unknown fields, they will be returned as a list of errors. fn try_from(options: &BabelOptions) -> Result { - let mut errors = Vec::::new(); - let env = options - .get_preset("env") - .flatten() - .and_then(|value| { - serde_json::from_value::(value) - .inspect_err(|err| report_error("env", err, true, &mut errors)) - .ok() - }) - .and_then(|env_options| EnvOptions::try_from(&env_options).ok()) + .presets + .env + .as_ref() + .and_then(|env_options| EnvOptions::try_from(env_options).ok()) .unwrap_or_default(); let regexp = RegExpOptions { @@ -195,16 +189,6 @@ impl TryFrom<&BabelOptions> for EnvOptions { class_properties: options.plugins.class_properties.or(env.es2022.class_properties), }; - if !errors.is_empty() { - return Err(errors); - } - Ok(Self { regexp, es2015, es2016, es2017, es2018, es2019, es2020, es2021, es2022 }) } } - -fn report_error(name: &str, err: &serde_json::Error, is_preset: bool, errors: &mut Vec) { - let message = - if is_preset { format!("preset-{name}: {err}",) } else { format!("{name}: {err}",) }; - errors.push(OxcDiagnostic::error(message).into()); -} diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs index f1d8ae567c2df..91074488303aa 100644 --- a/crates/oxc_transformer/src/options/mod.rs +++ b/crates/oxc_transformer/src/options/mod.rs @@ -4,7 +4,7 @@ mod env; use std::path::PathBuf; use env::EnvOptions; -use oxc_diagnostics::{Error, OxcDiagnostic}; +use oxc_diagnostics::Error; use crate::{ common::helper_loader::{HelperLoaderMode, HelperLoaderOptions}, @@ -73,34 +73,25 @@ impl TryFrom<&BabelOptions> for TransformOptions { fn try_from(options: &BabelOptions) -> Result { let mut errors = Vec::::new(); errors.extend(options.plugins.errors.iter().map(|err| Error::msg(err.clone()))); + errors.extend(options.presets.errors.iter().map(|err| Error::msg(err.clone()))); let assumptions = if options.assumptions.is_null() { CompilerAssumptions::default() } else { serde_json::from_value::(options.assumptions.clone()) - .inspect_err(|err| errors.push(OxcDiagnostic::error(err.to_string()).into())) + .map_err(|err| errors.push(Error::msg(err))) .unwrap_or_default() }; - let typescript = if options.has_preset("typescript") { - options.get_preset("typescript").and_then(|options| { - options - .map(|options| { - serde_json::from_value::(options) - .inspect_err(|err| report_error("typescript", err, true, &mut errors)) - .ok() - }) - .unwrap_or_default() - }) - } else { - options.plugins.typescript.clone() - } - .unwrap_or_default(); + let typescript = options + .presets + .typescript + .clone() + .or_else(|| options.plugins.typescript.clone()) + .unwrap_or_default(); - let jsx = if let Some(value) = options.get_preset("react").flatten() { - serde_json::from_value::(value) - .inspect_err(|err| report_error("react", err, true, &mut errors)) - .unwrap_or_default() + let jsx = if let Some(options) = &options.presets.jsx { + options.clone() } else { let mut jsx_options = if let Some(options) = &options.plugins.react_jsx_dev { options.clone() @@ -148,9 +139,3 @@ impl TryFrom<&BabelOptions> for TransformOptions { }) } } - -fn report_error(name: &str, err: &serde_json::Error, is_preset: bool, errors: &mut Vec) { - let message = - if is_preset { format!("preset-{name}: {err}",) } else { format!("{name}: {err}",) }; - errors.push(OxcDiagnostic::error(message).into()); -} diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index f1c9f0fae6646..f60fc8bd94d64 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -145,9 +145,7 @@ pub trait TestCase { } // Skip custom preset and flow - if options.presets.iter().any(|value| value.as_str().is_some_and(|s| s.starts_with("./"))) - || options.get_preset("flow").is_some() - { + if options.presets.unsupported.iter().any(|s| s.starts_with("./") || s == "flow") { return true; }