feat(transformer): add "_jsxFileName" variable in jsx source plugin (#3000)

This commit is contained in:
Boshen 2024-04-16 17:40:24 +08:00 committed by GitHub
parent 67045467c7
commit 85a3653994
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 127 additions and 53 deletions

View file

@ -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) {

View file

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

View file

@ -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),

View file

@ -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.

View file

@ -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,

View file

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

View file

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

View file

@ -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>,

View file

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

View file

@ -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(_)));

View file

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