mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
feat(transformer): add "_jsxFileName" variable in jsx source plugin (#3000)
This commit is contained in:
parent
67045467c7
commit
85a3653994
11 changed files with 127 additions and 53 deletions
|
|
@ -1,11 +1,16 @@
|
|||
use std::{cell::RefCell, mem, path::Path, rc::Rc};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::AstBuilder;
|
||||
use oxc_diagnostics::Error;
|
||||
use oxc_semantic::Semantic;
|
||||
|
||||
use crate::helpers::module_imports::ModuleImports;
|
||||
use crate::{helpers::module_imports::ModuleImports, TransformOptions};
|
||||
|
||||
pub type Ctx<'a> = Rc<TransformCtx<'a>>;
|
||||
|
||||
|
|
@ -17,6 +22,9 @@ pub struct TransformCtx<'a> {
|
|||
/// <https://babeljs.io/docs/options#filename>
|
||||
filename: String,
|
||||
|
||||
/// Source path in the form of `<CWD>/path/to/file/input.js`
|
||||
source_path: PathBuf,
|
||||
|
||||
errors: RefCell<Vec<Error>>,
|
||||
|
||||
// Helpers
|
||||
|
|
@ -25,14 +33,28 @@ pub struct TransformCtx<'a> {
|
|||
}
|
||||
|
||||
impl<'a> TransformCtx<'a> {
|
||||
pub fn new(allocator: &'a Allocator, source_path: &Path, semantic: Semantic<'a>) -> Self {
|
||||
let ast = AstBuilder::new(allocator);
|
||||
pub fn new(
|
||||
allocator: &'a Allocator,
|
||||
source_path: &Path,
|
||||
semantic: Semantic<'a>,
|
||||
options: &TransformOptions,
|
||||
) -> Self {
|
||||
let filename = source_path
|
||||
.file_stem() // omit file extension
|
||||
.map_or_else(|| String::from("unknown"), |name| name.to_string_lossy().to_string());
|
||||
let errors = RefCell::new(vec![]);
|
||||
let module_imports = ModuleImports::new(allocator);
|
||||
Self { ast, semantic, filename, errors, module_imports }
|
||||
|
||||
let source_path = source_path
|
||||
.strip_prefix(&options.cwd)
|
||||
.map_or_else(|_| source_path.to_path_buf(), |p| Path::new("<CWD>").join(p));
|
||||
|
||||
Self {
|
||||
ast: AstBuilder::new(allocator),
|
||||
semantic,
|
||||
filename,
|
||||
source_path,
|
||||
errors: RefCell::new(vec![]),
|
||||
module_imports: ModuleImports::new(allocator),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_errors(&self) -> Vec<Error> {
|
||||
|
|
@ -43,6 +65,10 @@ impl<'a> TransformCtx<'a> {
|
|||
&self.filename
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> &Path {
|
||||
&self.source_path
|
||||
}
|
||||
|
||||
/// Add an Error
|
||||
#[allow(unused)]
|
||||
pub fn error<T: Into<Error>>(&self, error: T) {
|
||||
|
|
|
|||
|
|
@ -129,8 +129,7 @@ impl<'a> ModuleImports<'a> {
|
|||
let decl = self.ast.variable_declarator(SPAN, var_kind, id, Some(init), false);
|
||||
self.ast.new_vec_single(decl)
|
||||
};
|
||||
let variable_declaration =
|
||||
self.ast.variable_declaration(SPAN, var_kind, decl, Modifiers::empty());
|
||||
Statement::Declaration(Declaration::VariableDeclaration(variable_declaration))
|
||||
let var_decl = self.ast.variable_declaration(SPAN, var_kind, decl, Modifiers::empty());
|
||||
Statement::Declaration(Declaration::VariableDeclaration(var_decl))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ impl<'a> Transformer<'a> {
|
|||
semantic: Semantic<'a>,
|
||||
options: TransformOptions,
|
||||
) -> Self {
|
||||
let ctx = Rc::new(TransformCtx::new(allocator, source_path, semantic));
|
||||
let ctx = Rc::new(TransformCtx::new(allocator, source_path, semantic, &options));
|
||||
Self {
|
||||
ctx: Rc::clone(&ctx),
|
||||
x0_typescript: TypeScript::new(options.typescript, &ctx),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
compiler_assumptions::CompilerAssumptions, react::ReactOptions, typescript::TypeScriptOptions,
|
||||
};
|
||||
|
||||
/// <https://babel.dev/docs/options>
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TransformOptions {
|
||||
//
|
||||
// Primary Options
|
||||
//
|
||||
/// The working directory that all paths in the programmatic options will be resolved relative to.
|
||||
pub cwd: PathBuf,
|
||||
|
||||
// Core
|
||||
/// Set assumptions in order to produce smaller output.
|
||||
/// For more information, check the [assumptions](https://babel.dev/docs/assumptions) documentation page.
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ pub struct ReactJsx<'a> {
|
|||
|
||||
ctx: Ctx<'a>,
|
||||
|
||||
jsx_self: ReactJsxSelf<'a>,
|
||||
jsx_source: ReactJsxSource<'a>,
|
||||
pub(super) jsx_self: ReactJsxSelf<'a>,
|
||||
pub(super) jsx_source: ReactJsxSource<'a>,
|
||||
|
||||
// States
|
||||
require_jsx_runtime: bool,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use oxc_syntax::NumberBase;
|
|||
use crate::context::Ctx;
|
||||
|
||||
const SOURCE: &str = "__source";
|
||||
const FILE_NAME_VAR: &str = "_jsxFileName";
|
||||
|
||||
/// [plugin-transform-react-jsx-source](https://babeljs.io/docs/babel-plugin-transform-react-jsx-source)
|
||||
///
|
||||
|
|
@ -20,18 +21,31 @@ const SOURCE: &str = "__source";
|
|||
/// TODO: get lineNumber and columnNumber from somewhere
|
||||
pub struct ReactJsxSource<'a> {
|
||||
ctx: Ctx<'a>,
|
||||
|
||||
/// Has `var _jsxFileName = "";` been added to program.statements?
|
||||
should_add_jsx_file_name_variable: bool,
|
||||
}
|
||||
|
||||
impl<'a> ReactJsxSource<'a> {
|
||||
pub fn new(ctx: &Ctx<'a>) -> Self {
|
||||
Self { ctx: Rc::clone(ctx) }
|
||||
Self { ctx: Rc::clone(ctx), should_add_jsx_file_name_variable: false }
|
||||
}
|
||||
|
||||
pub fn transform_jsx_opening_element(&self, elem: &mut JSXOpeningElement<'a>) {
|
||||
pub fn transform_program_on_exit(&mut self, program: &mut Program<'a>) {
|
||||
if !self.should_add_jsx_file_name_variable {
|
||||
return;
|
||||
}
|
||||
let statement = self.get_var_file_name_statement();
|
||||
program.body.insert(0, statement);
|
||||
}
|
||||
|
||||
pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
|
||||
self.should_add_jsx_file_name_variable = true;
|
||||
self.add_source_attribute(elem);
|
||||
}
|
||||
|
||||
pub fn get_object_property_kind_for_jsx_plugin(&self) -> ObjectPropertyKind<'a> {
|
||||
pub fn get_object_property_kind_for_jsx_plugin(&mut self) -> ObjectPropertyKind<'a> {
|
||||
self.should_add_jsx_file_name_variable = true;
|
||||
let kind = PropertyKind::Init;
|
||||
let ident = IdentifierName::new(SPAN, SOURCE.into());
|
||||
let key = self.ctx.ast.property_key_identifier(ident);
|
||||
|
|
@ -59,8 +73,8 @@ impl<'a> ReactJsxSource<'a> {
|
|||
let filename = {
|
||||
let name = IdentifierName::new(SPAN, "fileName".into());
|
||||
let key = self.ctx.ast.property_key_identifier(name);
|
||||
let string = StringLiteral::new(SPAN, self.ctx.ast.new_atom(self.ctx.filename()));
|
||||
let value = self.ctx.ast.literal_string_expression(string);
|
||||
let ident = self.ctx.ast.identifier_reference(SPAN, FILE_NAME_VAR);
|
||||
let value = self.ctx.ast.identifier_reference_expression(ident);
|
||||
self.ctx.ast.object_property(SPAN, kind, key, value, None, false, false, false)
|
||||
};
|
||||
|
||||
|
|
@ -86,4 +100,22 @@ impl<'a> ReactJsxSource<'a> {
|
|||
properties.push(ObjectPropertyKind::ObjectProperty(column_number));
|
||||
self.ctx.ast.object_expression(SPAN, properties, None)
|
||||
}
|
||||
|
||||
fn get_var_file_name_statement(&self) -> Statement<'a> {
|
||||
let var_kind = VariableDeclarationKind::Var;
|
||||
let id = {
|
||||
let ident = BindingIdentifier::new(SPAN, FILE_NAME_VAR.into());
|
||||
let ident = self.ctx.ast.binding_pattern_identifier(ident);
|
||||
self.ctx.ast.binding_pattern(ident, None, false)
|
||||
};
|
||||
let decl = {
|
||||
let string =
|
||||
self.ctx.ast.string_literal(SPAN, &self.ctx.source_path().to_string_lossy());
|
||||
let init = self.ctx.ast.literal_string_expression(string);
|
||||
let decl = self.ctx.ast.variable_declarator(SPAN, var_kind, id, Some(init), false);
|
||||
self.ctx.ast.new_vec_single(decl)
|
||||
};
|
||||
let var_decl = self.ctx.ast.variable_declaration(SPAN, var_kind, decl, Modifiers::empty());
|
||||
Statement::Declaration(Declaration::VariableDeclaration(var_decl))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ use oxc_ast::ast::*;
|
|||
|
||||
use crate::context::Ctx;
|
||||
|
||||
pub use self::{
|
||||
display_name::ReactDisplayName, jsx::ReactJsx, jsx_self::ReactJsxSelf,
|
||||
jsx_source::ReactJsxSource, options::ReactOptions,
|
||||
};
|
||||
pub use self::{display_name::ReactDisplayName, jsx::ReactJsx, options::ReactOptions};
|
||||
|
||||
/// [Preset React](https://babel.dev/docs/babel-preset-react)
|
||||
///
|
||||
|
|
@ -26,8 +23,6 @@ pub use self::{
|
|||
pub struct React<'a> {
|
||||
options: Rc<ReactOptions>,
|
||||
jsx: ReactJsx<'a>,
|
||||
jsx_self: ReactJsxSelf<'a>,
|
||||
jsx_source: ReactJsxSource<'a>,
|
||||
display_name: ReactDisplayName<'a>,
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +37,6 @@ impl<'a> React<'a> {
|
|||
Self {
|
||||
options: Rc::clone(&options),
|
||||
jsx: ReactJsx::new(&options, ctx),
|
||||
jsx_self: ReactJsxSelf::new(ctx),
|
||||
jsx_source: ReactJsxSource::new(ctx),
|
||||
display_name: ReactDisplayName::new(ctx),
|
||||
}
|
||||
}
|
||||
|
|
@ -52,9 +45,14 @@ impl<'a> React<'a> {
|
|||
// Transforms
|
||||
impl<'a> React<'a> {
|
||||
pub fn transform_program_on_exit(&mut self, program: &mut Program<'a>) {
|
||||
// TODO: PERF: These two transforms reallocathe program.statements,
|
||||
// they should be combined so that allocation is computed only once for program.statements.
|
||||
if self.options.jsx_plugin {
|
||||
self.jsx.transform_program_on_exit(program);
|
||||
}
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
self.jsx.jsx_source.transform_program_on_exit(program);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
|
||||
|
|
@ -96,12 +94,12 @@ impl<'a> React<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn transform_jsx_opening_element(&self, elem: &mut JSXOpeningElement<'a>) {
|
||||
pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
|
||||
if self.options.is_jsx_self_plugin_enabled() {
|
||||
self.jsx_self.transform_jsx_opening_element(elem);
|
||||
self.jsx.jsx_self.transform_jsx_opening_element(elem);
|
||||
}
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
self.jsx_source.transform_jsx_opening_element(elem);
|
||||
self.jsx.jsx_source.transform_jsx_opening_element(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
|
@ -7,6 +7,7 @@ use serde_json::Value;
|
|||
#[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>,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Passed: 133/219
|
||||
Passed: 134/219
|
||||
|
||||
# All Passed:
|
||||
* babel-plugin-transform-react-jsx-source
|
||||
|
|
@ -85,8 +85,7 @@ Passed: 133/219
|
|||
* regression/15768/input.ts
|
||||
* variable-declaration/non-null-in-optional-chain/input.ts
|
||||
|
||||
# babel-preset-react (7/11)
|
||||
* preset-options/development/input.js
|
||||
# babel-preset-react (8/11)
|
||||
* preset-options/development-runtime-automatic/input.js
|
||||
* preset-options/development-runtime-automatic-windows/input.js
|
||||
* preset-options/development-windows/input.js
|
||||
|
|
|
|||
|
|
@ -28,18 +28,22 @@ pub struct TestRunner {
|
|||
options: TestRunnerOptions,
|
||||
}
|
||||
|
||||
fn root() -> PathBuf {
|
||||
project_root().join("tasks/coverage/babel/packages")
|
||||
fn babel_root() -> PathBuf {
|
||||
project_root().join("tasks").join("coverage").join("babel")
|
||||
}
|
||||
|
||||
fn oxc_test_root() -> PathBuf {
|
||||
project_root().join("tasks/transform_conformance/tests")
|
||||
fn packages_root() -> PathBuf {
|
||||
babel_root().join("packages")
|
||||
}
|
||||
|
||||
fn snap_root() -> PathBuf {
|
||||
project_root().join("tasks/transform_conformance")
|
||||
}
|
||||
|
||||
fn oxc_test_root() -> PathBuf {
|
||||
snap_root().join("tests")
|
||||
}
|
||||
|
||||
fn fixture_root() -> PathBuf {
|
||||
snap_root().join("fixtures")
|
||||
}
|
||||
|
|
@ -138,7 +142,7 @@ impl TestRunner {
|
|||
/// # Panics
|
||||
pub fn run(self) {
|
||||
for (root, snapshot, exec_snapshot) in &[
|
||||
(root(), CONFORMANCE_SNAPSHOT, EXEC_SNAPSHOT),
|
||||
(packages_root(), CONFORMANCE_SNAPSHOT, EXEC_SNAPSHOT),
|
||||
(oxc_test_root(), OXC_CONFORMANCE_SNAPSHOT, OXC_EXEC_SNAPSHOT),
|
||||
] {
|
||||
let (transform_paths, exec_files) =
|
||||
|
|
@ -160,6 +164,7 @@ impl TestRunner {
|
|||
root: &Path,
|
||||
filter: Option<&String>,
|
||||
) -> (IndexMap<String, Vec<TestCaseKind>>, IndexMap<String, Vec<TestCaseKind>>) {
|
||||
let cwd = babel_root();
|
||||
// use `IndexMap` to keep the order of the test cases the same in insert order.
|
||||
let mut transform_files = IndexMap::<String, Vec<TestCaseKind>>::new();
|
||||
let mut exec_files = IndexMap::<String, Vec<TestCaseKind>>::new();
|
||||
|
|
@ -180,7 +185,8 @@ impl TestRunner {
|
|||
if EXCLUDE_TESTS.iter().any(|p| path.to_string_lossy().contains(p)) {
|
||||
return None;
|
||||
}
|
||||
TestCaseKind::new(path).filter(|test_case| !test_case.skip_test_case())
|
||||
TestCaseKind::new(&cwd, path)
|
||||
.filter(|test_case| !test_case.skip_test_case())
|
||||
})
|
||||
.partition(|p| matches!(p, TestCaseKind::Transform(_)));
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use oxc_span::{SourceType, VALID_EXTENSIONS};
|
|||
use oxc_tasks_common::{normalize_path, print_diff_in_terminal, BabelOptions};
|
||||
use oxc_transformer::{ReactOptions, TransformOptions, Transformer, TypeScriptOptions};
|
||||
|
||||
use crate::{fixture_root, root, TestRunnerEnv, PLUGINS_NOT_SUPPORTED_YET};
|
||||
use crate::{fixture_root, packages_root, TestRunnerEnv, PLUGINS_NOT_SUPPORTED_YET};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TestCaseKind {
|
||||
|
|
@ -24,25 +24,25 @@ pub enum TestCaseKind {
|
|||
}
|
||||
|
||||
impl TestCaseKind {
|
||||
pub fn new(path: &Path) -> Option<Self> {
|
||||
pub fn new(cwd: &Path, path: &Path) -> Option<Self> {
|
||||
// in `exec` directory
|
||||
if path.parent().is_some_and(|path| path.file_name().is_some_and(|n| n == "exec"))
|
||||
&& path.extension().is_some_and(|ext| VALID_EXTENSIONS.contains(&ext.to_str().unwrap()))
|
||||
{
|
||||
return Some(Self::Exec(ExecTestCase::new(path)));
|
||||
return Some(Self::Exec(ExecTestCase::new(cwd, path)));
|
||||
}
|
||||
// named `exec.[ext]`
|
||||
if path.file_stem().is_some_and(|name| name == "exec")
|
||||
&& path.extension().is_some_and(|ext| VALID_EXTENSIONS.contains(&ext.to_str().unwrap()))
|
||||
{
|
||||
return Some(Self::Exec(ExecTestCase::new(path)));
|
||||
return Some(Self::Exec(ExecTestCase::new(cwd, path)));
|
||||
}
|
||||
|
||||
// named `input.[ext]``
|
||||
if path.file_stem().is_some_and(|name| name == "input")
|
||||
&& path.extension().is_some_and(|ext| VALID_EXTENSIONS.contains(&ext.to_str().unwrap()))
|
||||
{
|
||||
return Some(Self::Transform(ConformanceTestCase::new(path)));
|
||||
return Some(Self::Transform(ConformanceTestCase::new(cwd, path)));
|
||||
}
|
||||
|
||||
None
|
||||
|
|
@ -95,6 +95,7 @@ fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOpti
|
|||
};
|
||||
|
||||
Ok(TransformOptions {
|
||||
cwd: options.cwd.clone().unwrap(),
|
||||
assumptions: serde_json::from_value(options.assumptions.clone()).unwrap_or_default(),
|
||||
typescript: options
|
||||
.get_plugin("transform-typescript")
|
||||
|
|
@ -106,7 +107,7 @@ fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOpti
|
|||
}
|
||||
|
||||
pub trait TestCase {
|
||||
fn new(path: &Path) -> Self;
|
||||
fn new(cwd: &Path, path: &Path) -> Self;
|
||||
|
||||
fn options(&self) -> &BabelOptions;
|
||||
|
||||
|
|
@ -213,8 +214,9 @@ pub struct ConformanceTestCase {
|
|||
}
|
||||
|
||||
impl TestCase for ConformanceTestCase {
|
||||
fn new(path: &Path) -> Self {
|
||||
let options = BabelOptions::from_path(path.parent().unwrap());
|
||||
fn new(cwd: &Path, path: &Path) -> Self {
|
||||
let mut options = BabelOptions::from_path(path.parent().unwrap());
|
||||
options.cwd.replace(cwd.to_path_buf());
|
||||
let transform_options = transform_options(&options);
|
||||
Self { path: path.to_path_buf(), options, transform_options }
|
||||
}
|
||||
|
|
@ -361,10 +363,11 @@ impl ExecTestCase {
|
|||
|
||||
fn write_to_test_files(&self, content: &str) -> PathBuf {
|
||||
let allocator = Allocator::default();
|
||||
let new_file_name: String = normalize_path(self.path.strip_prefix(&root()).unwrap())
|
||||
.split('/')
|
||||
.collect::<Vec<&str>>()
|
||||
.join("-");
|
||||
let new_file_name: String =
|
||||
normalize_path(self.path.strip_prefix(&packages_root()).unwrap())
|
||||
.split('/')
|
||||
.collect::<Vec<&str>>()
|
||||
.join("-");
|
||||
|
||||
let mut target_path = fixture_root().join(new_file_name);
|
||||
target_path.set_extension("test.js");
|
||||
|
|
@ -385,8 +388,9 @@ impl ExecTestCase {
|
|||
}
|
||||
|
||||
impl TestCase for ExecTestCase {
|
||||
fn new(path: &Path) -> Self {
|
||||
let options = BabelOptions::from_path(path.parent().unwrap());
|
||||
fn new(cwd: &Path, path: &Path) -> Self {
|
||||
let mut options = BabelOptions::from_path(path.parent().unwrap());
|
||||
options.cwd.replace(cwd.to_path_buf());
|
||||
let transform_options = transform_options(&options);
|
||||
Self { path: path.to_path_buf(), options, transform_options }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue