refactor(transformer): deserialize BabelOptions::presets (#7047)

This commit is contained in:
Boshen 2024-11-01 06:56:27 +00:00
parent 52c20d633c
commit f83a760d8a
6 changed files with 249 additions and 243 deletions

View file

@ -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
///
/// <https://babel.dev/docs/options#plugin-and-preset-options>
@ -28,7 +27,7 @@ pub struct BabelOptions {
pub plugins: BabelPlugins,
#[serde(default)]
pub presets: Vec<Value>, // Can be a string or an array
pub presets: BabelPresets,
// Misc options
pub source_type: Option<String>,
@ -59,6 +58,38 @@ pub struct BabelOptions {
pub external_helpers: bool,
}
/// <https://babeljs.io/docs/options#pluginpreset-entries>
#[derive(Debug, Deserialize)]
struct PluginPresetEntries(Vec<PluginPresetEntry>);
/// <https://babeljs.io/docs/options#pluginpreset-entries>
#[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<T: DeserializeOwned + Default>(self) -> Result<T, String> {
match self {
Self::String(_) | Self::Vec1(_) => Ok(T::default()),
Self::Tuple(name, v) | Self::Triple(name, v, _) => {
serde_json::from_value::<T>(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<Self> = 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::<BabelOptions>(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::<BabelPlugins>(plugins_json).unwrap();
options.plugins = serde_json::from_value::<BabelPlugins>(plugins_json)
.unwrap_or_else(|err| panic!("{err:?}\n{path:?}"));
}
if let Some(presets_json) = presets_json {
options.presets = serde_json::from_value::<BabelPresets>(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<Option<Value>> {
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<Option<Value>> {
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<String>,
pub unsupported: Vec<String>,
// syntax
pub syntax_typescript: Option<SyntaxTypeScriptOptions>,
pub syntax_jsx: bool,
// decorators
pub syntax_decorators: Option<SyntaxDecoratorOptions>,
pub proposal_decorators: Option<SyntaxDecoratorOptions>,
// ts
pub typescript: Option<TypeScriptOptions>,
// jsx
pub react_jsx: Option<JsxOptions>,
pub react_jsx_dev: Option<JsxOptions>,
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<ArrowFunctionsOptions>,
// ES2016
pub exponentiation_operator: bool,
// ES2017
pub async_to_generator: bool,
// ES2018
pub object_rest_spread: Option<ObjectRestSpreadOptions>,
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<ClassPropertiesOptions>,
}
/// <https://babeljs.io/docs/options#pluginpreset-entries>
#[derive(Debug, Deserialize)]
struct PluginPresetEntries(Vec<PluginPresetEntry>);
impl TryFrom<PluginPresetEntries> for BabelPlugins {
type Error = String;
fn try_from(entries: PluginPresetEntries) -> Result<Self, Self::Error> {
let mut p = BabelPlugins::default();
for entry in entries.0 {
match entry.name() {
"typescript" | "syntax-typescript" => {
p.syntax_typescript = Some(entry.value::<SyntaxTypeScriptOptions>()?);
}
"jsx" | "syntax-jsx" => p.syntax_jsx = true,
"syntax-decorators" => {
p.syntax_decorators = Some(entry.value::<SyntaxDecoratorOptions>()?);
}
"proposal-decorators" => {
p.proposal_decorators = Some(entry.value::<SyntaxDecoratorOptions>()?);
}
"transform-typescript" => {
p.typescript =
entry.value::<TypeScriptOptions>().map_err(|err| p.errors.push(err)).ok();
}
"transform-react-jsx" => {
p.react_jsx =
entry.value::<JsxOptions>().map_err(|err| p.errors.push(err)).ok();
}
"transform-react-jsx-development" => {
p.react_jsx_dev =
entry.value::<JsxOptions>().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::<ArrowFunctionsOptions>()
.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::<ObjectRestSpreadOptions>()
.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::<ClassPropertiesOptions>()
.inspect_err(|err| p.errors.push(err.to_string()))
.ok();
}
s => p.unsupported.push(s.to_string()),
}
}
Ok(p)
}
}
/// <https://babeljs.io/docs/options#pluginpreset-entries>
#[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<T: DeserializeOwned + Default>(self) -> Result<T, String> {
match self {
Self::String(_) | Self::Vec1(_) => Ok(T::default()),
Self::Tuple(name, v) => {
serde_json::from_value::<T>(v).map_err(|err| format!("{name}: {err}"))
}
}
}
}

View file

@ -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<String>,
pub unsupported: Vec<String>,
// syntax
pub syntax_typescript: Option<SyntaxTypeScriptOptions>,
pub syntax_jsx: bool,
// decorators
pub syntax_decorators: Option<SyntaxDecoratorOptions>,
pub proposal_decorators: Option<SyntaxDecoratorOptions>,
// ts
pub typescript: Option<TypeScriptOptions>,
// jsx
pub react_jsx: Option<JsxOptions>,
pub react_jsx_dev: Option<JsxOptions>,
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<ArrowFunctionsOptions>,
// ES2016
pub exponentiation_operator: bool,
// ES2017
pub async_to_generator: bool,
// ES2018
pub object_rest_spread: Option<ObjectRestSpreadOptions>,
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<ClassPropertiesOptions>,
}
impl TryFrom<PluginPresetEntries> for BabelPlugins {
type Error = String;
fn try_from(entries: PluginPresetEntries) -> Result<Self, Self::Error> {
let mut p = Self::default();
for entry in entries.0 {
match entry.name() {
"typescript" | "syntax-typescript" => {
p.syntax_typescript = Some(entry.value::<SyntaxTypeScriptOptions>()?);
}
"jsx" | "syntax-jsx" => p.syntax_jsx = true,
"syntax-decorators" => {
p.syntax_decorators = Some(entry.value::<SyntaxDecoratorOptions>()?);
}
"proposal-decorators" => {
p.proposal_decorators = Some(entry.value::<SyntaxDecoratorOptions>()?);
}
"transform-typescript" => {
p.typescript =
entry.value::<TypeScriptOptions>().map_err(|err| p.errors.push(err)).ok();
}
"transform-react-jsx" => {
p.react_jsx =
entry.value::<JsxOptions>().map_err(|err| p.errors.push(err)).ok();
}
"transform-react-jsx-development" => {
p.react_jsx_dev =
entry.value::<JsxOptions>().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::<ArrowFunctionsOptions>()
.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::<ObjectRestSpreadOptions>()
.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::<ClassPropertiesOptions>()
.map_err(|err| p.errors.push(err))
.ok();
}
s => p.unsupported.push(s.to_string()),
}
}
Ok(p)
}
}

View file

@ -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<String>,
pub unsupported: Vec<String>,
pub env: Option<BabelEnvOptions>,
pub jsx: Option<JsxOptions>,
pub typescript: Option<TypeScriptOptions>,
}
impl TryFrom<PluginPresetEntries> for BabelPresets {
type Error = String;
fn try_from(entries: PluginPresetEntries) -> Result<Self, Self::Error> {
let mut p = Self::default();
for entry in entries.0 {
match entry.name() {
"env" => {
p.env = entry.value::<BabelEnvOptions>().map_err(|err| p.errors.push(err)).ok();
}
"typescript" => {
p.typescript =
entry.value::<TypeScriptOptions>().map_err(|err| p.errors.push(err)).ok();
}
"react" => {
p.jsx = entry.value::<JsxOptions>().map_err(|err| p.errors.push(err)).ok();
}
s => p.unsupported.push(s.to_string()),
}
}
Ok(p)
}
}

View file

@ -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<Self, Self::Error> {
let mut errors = Vec::<Error>::new();
let env = options
.get_preset("env")
.flatten()
.and_then(|value| {
serde_json::from_value::<BabelEnvOptions>(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<Error>) {
let message =
if is_preset { format!("preset-{name}: {err}",) } else { format!("{name}: {err}",) };
errors.push(OxcDiagnostic::error(message).into());
}

View file

@ -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<Self, Self::Error> {
let mut errors = Vec::<Error>::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::<CompilerAssumptions>(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::<TypeScriptOptions>(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::<JsxOptions>(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<Error>) {
let message =
if is_preset { format!("preset-{name}: {err}",) } else { format!("{name}: {err}",) };
errors.push(OxcDiagnostic::error(message).into());
}

View file

@ -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;
}