feat(transformer): support from_babel_options in TransformOptions (#3301)

Move `BabelOptions` to Transformer. The `output.json` is a standard babel configuration. We can reuse BabelOptions to read [babel.config.json](https://babeljs.io/docs/configuration#babelconfigjson) or our configuration(maybe oxc.config.json)

The current `from_babel_options` implementation is copied from the `transform_options` in `test_case.rs`, which I'll completely reimplement next
This commit is contained in:
Dunqing 2024-05-16 10:10:39 +00:00
parent bd8a0ddb7f
commit 9ee962add8
10 changed files with 201 additions and 202 deletions

4
Cargo.lock generated
View file

@ -1659,8 +1659,6 @@ version = "0.0.0"
dependencies = [ dependencies = [
"console", "console",
"project-root", "project-root",
"serde",
"serde_json",
"similar", "similar",
"ureq", "ureq",
"url", "url",
@ -1679,7 +1677,6 @@ dependencies = [
"oxc_tasks_common", "oxc_tasks_common",
"oxc_transformer", "oxc_transformer",
"pico-args", "pico-args",
"serde",
"serde_json", "serde_json",
"walkdir", "walkdir",
] ]
@ -1700,6 +1697,7 @@ dependencies = [
"ropey", "ropey",
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_json",
] ]
[[package]] [[package]]

View file

@ -29,6 +29,7 @@ oxc_traverse = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
ropey = { workspace = true } ropey = { workspace = true }
[dev-dependencies] [dev-dependencies]

View file

@ -31,8 +31,8 @@ use oxc_span::SourceType;
use oxc_traverse::{traverse_mut, Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut, Traverse, TraverseCtx};
pub use crate::{ pub use crate::{
compiler_assumptions::CompilerAssumptions, es2015::ES2015Options, options::TransformOptions, compiler_assumptions::CompilerAssumptions, es2015::ES2015Options, options::BabelOptions,
react::ReactOptions, typescript::TypeScriptOptions, options::TransformOptions, react::ReactOptions, typescript::TypeScriptOptions,
}; };
use crate::{ use crate::{

View file

@ -1,4 +1,7 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use crate::{ use crate::{
compiler_assumptions::CompilerAssumptions, es2015::ES2015Options, react::ReactOptions, compiler_assumptions::CompilerAssumptions, es2015::ES2015Options, react::ReactOptions,
@ -28,3 +31,188 @@ pub struct TransformOptions {
pub es2015: ES2015Options, pub es2015: ES2015Options,
} }
impl TransformOptions {
/// # Panics
/// Panics if the options are invalid.
/// # Errors
pub fn from_babel_options(options: &BabelOptions) -> serde_json::Result<Self> {
fn get_options<T: Default + DeserializeOwned>(
value: Option<Value>,
) -> serde_json::Result<T> {
match value {
Some(v) => serde_json::from_value::<T>(v),
None => Ok(T::default()),
}
}
let react = if let Some(options) = options.get_preset("react") {
get_options::<ReactOptions>(options)?
} else {
let jsx_plugin = options.get_plugin("transform-react-jsx");
let jsx_development_plugin = options.get_plugin("transform-react-jsx-development");
let has_jsx_plugin =
jsx_plugin.as_ref().is_some() || jsx_development_plugin.as_ref().is_some();
let mut react_options = jsx_plugin
.map(get_options::<ReactOptions>)
.or_else(|| jsx_development_plugin.map(get_options::<ReactOptions>))
.transpose()?
.unwrap_or_default();
react_options.development =
options.get_plugin("transform-react-jsx-development").is_some();
react_options.jsx_plugin = has_jsx_plugin;
react_options.display_name_plugin =
options.get_plugin("transform-react-display-name").is_some();
react_options.jsx_self_plugin =
options.get_plugin("transform-react-jsx-self").is_some();
react_options.jsx_source_plugin =
options.get_plugin("transform-react-jsx-source").is_some();
react_options
};
let es2015 = ES2015Options {
arrow_function: options
.get_plugin("transform-arrow-functions")
.map(get_options)
.transpose()?,
};
Ok(Self {
cwd: options.cwd.clone().unwrap(),
assumptions: serde_json::from_value(options.assumptions.clone()).unwrap_or_default(),
typescript: options
.get_plugin("transform-typescript")
.map(get_options::<TypeScriptOptions>)
.transpose()?
.unwrap_or_default(),
react,
es2015,
})
}
}
/// Babel options
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BabelOptions {
pub cwd: Option<PathBuf>,
pub source_type: Option<String>,
#[serde(default)]
pub plugins: Vec<Value>, // Can be a string or an array
#[serde(default)]
pub presets: Vec<Value>, // Can be a string or an array
#[serde(default)]
pub assumptions: Value,
// Test options
pub throws: Option<String>,
#[serde(rename = "BABEL_8_BREAKING")]
pub babel_8_breaking: Option<bool>,
/// Babel test helper for running tests on specific operating systems
pub os: Option<Vec<TestOs>>,
// Parser options for babel-parser
#[serde(default)]
pub allow_return_outside_function: bool,
#[serde(default)]
pub allow_await_outside_function: bool,
#[serde(default)]
pub allow_undeclared_exports: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TestOs {
Linux,
Win32,
Windows,
Darwin,
}
impl TestOs {
pub fn is_windows(&self) -> bool {
matches!(self, Self::Win32 | Self::Windows)
}
}
impl BabelOptions {
/// Read options.json and merge them with options.json from ancestors directories.
/// # Panics
pub fn from_test_path(path: &Path) -> Self {
let mut options_json: Option<Self> = None;
for path in path.ancestors().take(3) {
let file = path.join("options.json");
if !file.exists() {
continue;
}
let file = std::fs::read_to_string(&file).unwrap();
let new_json: Self = serde_json::from_str(&file).unwrap();
if let Some(existing_json) = options_json.as_mut() {
if existing_json.source_type.is_none() {
if let Some(source_type) = new_json.source_type {
existing_json.source_type = Some(source_type);
}
}
if existing_json.throws.is_none() {
if let Some(throws) = new_json.throws {
existing_json.throws = Some(throws);
}
}
existing_json.plugins.extend(new_json.plugins);
} else {
options_json = Some(new_json);
}
}
options_json.unwrap_or_default()
}
pub fn is_jsx(&self) -> bool {
self.plugins.iter().any(|v| v.as_str().is_some_and(|v| v == "jsx"))
}
pub fn is_typescript(&self) -> bool {
self.plugins.iter().any(|v| {
let string_value = v.as_str().is_some_and(|v| v == "typescript");
let array_value = v.get(0).and_then(Value::as_str).is_some_and(|s| s == "typescript");
string_value || array_value
})
}
pub fn is_typescript_definition(&self) -> bool {
self.plugins.iter().filter_map(Value::as_array).any(|p| {
let typescript = p.first().and_then(Value::as_str).is_some_and(|s| s == "typescript");
let dts = p
.get(1)
.and_then(Value::as_object)
.and_then(|v| v.get("dts"))
.and_then(Value::as_bool)
.is_some_and(|v| v);
typescript && dts
})
}
pub fn is_module(&self) -> bool {
self.source_type.as_ref().map_or(false, |s| matches!(s.as_str(), "module" | "unambiguous"))
}
/// Returns
/// * `Some<None>` if the plugin exists without a config
/// * `Some<Some<Value>>` if the plugin exists with a config
/// * `None` if the plugin does not exist
pub fn get_plugin(&self, name: &str) -> Option<Option<Value>> {
self.plugins.iter().find_map(|v| Self::get_value(v, name))
}
pub fn get_preset(&self, name: &str) -> Option<Option<Value>> {
self.presets.iter().find_map(|v| Self::get_value(v, name))
}
#[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,
}
}
}

View file

@ -16,8 +16,6 @@ doctest = false
console = { workspace = true } console = { workspace = true }
project-root = { workspace = true } project-root = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
similar = { workspace = true } similar = { workspace = true }
ureq = { workspace = true, features = ["tls", "json"] } ureq = { workspace = true, features = ["tls", "json"] }

View file

@ -1,128 +0,0 @@
use std::path::{Path, PathBuf};
use serde::Deserialize;
use serde_json::Value;
/// Babel options.json for tests
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BabelOptions {
pub cwd: Option<PathBuf>,
#[serde(rename = "BABEL_8_BREAKING")]
pub babel_8_breaking: Option<bool>,
pub source_type: Option<String>,
pub throws: Option<String>,
#[serde(default)]
pub plugins: Vec<Value>, // Can be a string or an array
#[serde(default)]
pub presets: Vec<Value>, // Can be a string or an array
#[serde(default)]
pub allow_return_outside_function: bool,
#[serde(default)]
pub allow_await_outside_function: bool,
#[serde(default)]
pub allow_undeclared_exports: bool,
#[serde(default)]
pub assumptions: Value,
/// Babel test helper for running tests on specific operating systems
pub os: Option<Vec<TestOs>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TestOs {
Linux,
Win32,
Windows,
Darwin,
}
impl TestOs {
pub fn is_windows(&self) -> bool {
matches!(self, Self::Win32 | Self::Windows)
}
}
impl BabelOptions {
/// Read options.json and merge them with options.json from ancestors directories.
/// # Panics
pub fn from_path(path: &Path) -> Self {
let mut options_json: Option<Self> = None;
for path in path.ancestors().take(3) {
let file = path.join("options.json");
if !file.exists() {
continue;
}
let file = std::fs::read_to_string(&file).unwrap();
let new_json: Self = serde_json::from_str(&file).unwrap();
if let Some(existing_json) = options_json.as_mut() {
if existing_json.source_type.is_none() {
if let Some(source_type) = new_json.source_type {
existing_json.source_type = Some(source_type);
}
}
if existing_json.throws.is_none() {
if let Some(throws) = new_json.throws {
existing_json.throws = Some(throws);
}
}
existing_json.plugins.extend(new_json.plugins);
} else {
options_json = Some(new_json);
}
}
options_json.unwrap_or_default()
}
pub fn is_jsx(&self) -> bool {
self.plugins.iter().any(|v| v.as_str().is_some_and(|v| v == "jsx"))
}
pub fn is_typescript(&self) -> bool {
self.plugins.iter().any(|v| {
let string_value = v.as_str().is_some_and(|v| v == "typescript");
let array_value = v.get(0).and_then(Value::as_str).is_some_and(|s| s == "typescript");
string_value || array_value
})
}
pub fn is_typescript_definition(&self) -> bool {
self.plugins.iter().filter_map(Value::as_array).any(|p| {
let typescript = p.first().and_then(Value::as_str).is_some_and(|s| s == "typescript");
let dts = p
.get(1)
.and_then(Value::as_object)
.and_then(|v| v.get("dts"))
.and_then(Value::as_bool)
.is_some_and(|v| v);
typescript && dts
})
}
pub fn is_module(&self) -> bool {
self.source_type.as_ref().map_or(false, |s| matches!(s.as_str(), "module" | "unambiguous"))
}
/// Returns
/// * `Some<None>` if the plugin exists without a config
/// * `Some<Some<Value>>` if the plugin exists with a config
/// * `None` if the plugin does not exist
pub fn get_plugin(&self, name: &str) -> Option<Option<Value>> {
self.plugins.iter().find_map(|v| Self::get_value(v, name))
}
pub fn get_preset(&self, name: &str) -> Option<Option<Value>> {
self.presets.iter().find_map(|v| Self::get_value(v, name))
}
#[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,
}
}
}

View file

@ -1,17 +1,11 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
mod babel;
mod diff; mod diff;
mod request; mod request;
mod snapshot; mod snapshot;
mod test_file; mod test_file;
pub use crate::{ pub use crate::{request::agent, snapshot::Snapshot, test_file::*};
babel::{BabelOptions, TestOs},
request::agent,
snapshot::Snapshot,
test_file::*,
};
pub use diff::print_diff_in_terminal; pub use diff::print_diff_in_terminal;
/// # Panics /// # Panics

View file

@ -1,10 +1,10 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use oxc_transformer::BabelOptions;
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value; use serde_json::Value;
use oxc_span::SourceType; use oxc_span::SourceType;
use oxc_tasks_common::BabelOptions;
use crate::{ use crate::{
project_root, project_root,
@ -128,7 +128,7 @@ impl Case for BabelCase {
/// # Panics /// # Panics
fn new(path: PathBuf, code: String) -> Self { fn new(path: PathBuf, code: String) -> Self {
let dir = project_root().join(FIXTURES_PATH).join(&path); let dir = project_root().join(FIXTURES_PATH).join(&path);
let options = BabelOptions::from_path(dir.parent().unwrap()); let options = BabelOptions::from_test_path(dir.parent().unwrap());
let source_type = SourceType::from_path(&path) let source_type = SourceType::from_path(&path)
.unwrap() .unwrap()
.with_script(true) .with_script(true)

View file

@ -32,5 +32,4 @@ oxc_diagnostics = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }
pico-args = { workspace = true } pico-args = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }

View file

@ -3,18 +3,13 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use serde::de::DeserializeOwned;
use serde_json::Value;
use oxc_allocator::Allocator; use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions}; use oxc_codegen::{Codegen, CodegenOptions};
use oxc_diagnostics::{Error, OxcDiagnostic}; use oxc_diagnostics::{Error, OxcDiagnostic};
use oxc_parser::Parser; use oxc_parser::Parser;
use oxc_span::{SourceType, VALID_EXTENSIONS}; use oxc_span::{SourceType, VALID_EXTENSIONS};
use oxc_tasks_common::{normalize_path, print_diff_in_terminal, BabelOptions}; use oxc_tasks_common::{normalize_path, print_diff_in_terminal};
use oxc_transformer::{ use oxc_transformer::{BabelOptions, TransformOptions, Transformer};
ES2015Options, ReactOptions, TransformOptions, Transformer, TypeScriptOptions,
};
use crate::{ use crate::{
constants::{PLUGINS_NOT_SUPPORTED_YET, SKIP_TESTS}, constants::{PLUGINS_NOT_SUPPORTED_YET, SKIP_TESTS},
@ -75,53 +70,7 @@ impl TestCaseKind {
} }
fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOptions> { fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOptions> {
fn get_options<T: Default + DeserializeOwned>(value: Option<Value>) -> serde_json::Result<T> { TransformOptions::from_babel_options(options)
match value {
Some(v) => serde_json::from_value::<T>(v),
None => Ok(T::default()),
}
}
let react = if let Some(options) = options.get_preset("react") {
get_options::<ReactOptions>(options)?
} else {
let jsx_plugin = options.get_plugin("transform-react-jsx");
let jsx_development_plugin = options.get_plugin("transform-react-jsx-development");
let has_jsx_plugin =
jsx_plugin.as_ref().is_some() || jsx_development_plugin.as_ref().is_some();
let mut react_options = jsx_plugin
.map(get_options::<ReactOptions>)
.or_else(|| jsx_development_plugin.map(get_options::<ReactOptions>))
.transpose()?
.unwrap_or_default();
react_options.development = options.get_plugin("transform-react-jsx-development").is_some();
react_options.jsx_plugin = has_jsx_plugin;
react_options.display_name_plugin =
options.get_plugin("transform-react-display-name").is_some();
react_options.jsx_self_plugin = options.get_plugin("transform-react-jsx-self").is_some();
react_options.jsx_source_plugin =
options.get_plugin("transform-react-jsx-source").is_some();
react_options
};
let es2015 = ES2015Options {
arrow_function: options
.get_plugin("transform-arrow-functions")
.map(get_options)
.transpose()?,
};
Ok(TransformOptions {
cwd: options.cwd.clone().unwrap(),
assumptions: serde_json::from_value(options.assumptions.clone()).unwrap_or_default(),
typescript: options
.get_plugin("transform-typescript")
.map(get_options::<TypeScriptOptions>)
.transpose()?
.unwrap_or_default(),
react,
es2015,
})
} }
pub trait TestCase { pub trait TestCase {
@ -242,7 +191,7 @@ pub struct ConformanceTestCase {
impl TestCase for ConformanceTestCase { impl TestCase for ConformanceTestCase {
fn new(cwd: &Path, path: &Path) -> Self { fn new(cwd: &Path, path: &Path) -> Self {
let mut options = BabelOptions::from_path(path.parent().unwrap()); let mut options = BabelOptions::from_test_path(path.parent().unwrap());
options.cwd.replace(cwd.to_path_buf()); options.cwd.replace(cwd.to_path_buf());
let transform_options = transform_options(&options); let transform_options = transform_options(&options);
Self { path: path.to_path_buf(), options, transform_options } Self { path: path.to_path_buf(), options, transform_options }
@ -437,7 +386,7 @@ impl ExecTestCase {
impl TestCase for ExecTestCase { impl TestCase for ExecTestCase {
fn new(cwd: &Path, path: &Path) -> Self { fn new(cwd: &Path, path: &Path) -> Self {
let mut options = BabelOptions::from_path(path.parent().unwrap()); let mut options = BabelOptions::from_test_path(path.parent().unwrap());
options.cwd.replace(cwd.to_path_buf()); options.cwd.replace(cwd.to_path_buf());
let transform_options = transform_options(&options); let transform_options = transform_options(&options);
Self { path: path.to_path_buf(), options, transform_options } Self { path: path.to_path_buf(), options, transform_options }