mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
feat(transformer/react): support development mode (#3143)
This commit is contained in:
parent
f0cbbbe28c
commit
18d853bb2b
5 changed files with 166 additions and 76 deletions
|
|
@ -45,7 +45,6 @@ pub struct ReactJsx<'a> {
|
|||
// States
|
||||
require_jsx_runtime: bool,
|
||||
jsx_runtime_importer: CompactStr,
|
||||
default_runtime: ReactJsxRuntime,
|
||||
|
||||
import_jsx: bool,
|
||||
import_jsxs: bool,
|
||||
|
|
@ -59,9 +58,15 @@ impl<'a> ReactJsx<'a> {
|
|||
let default_runtime = options.runtime;
|
||||
let jsx_runtime_importer =
|
||||
if options.import_source == "react" || default_runtime.is_classic() {
|
||||
CompactStr::from("react/jsx-runtime")
|
||||
let source =
|
||||
if options.development { "react/jsx-dev-runtime" } else { "react/jsx-runtime" };
|
||||
CompactStr::from(source)
|
||||
} else {
|
||||
CompactStr::from(format!("{}/jsx-runtime", options.import_source))
|
||||
CompactStr::from(format!(
|
||||
"{}/jsx-{}runtime",
|
||||
options.import_source,
|
||||
if options.development { "dev-" } else { "" }
|
||||
))
|
||||
};
|
||||
|
||||
Self {
|
||||
|
|
@ -75,7 +80,6 @@ impl<'a> ReactJsx<'a> {
|
|||
import_jsxs: false,
|
||||
import_fragment: false,
|
||||
import_create_element: false,
|
||||
default_runtime,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +111,11 @@ impl<'a> ReactJsx<'a> {
|
|||
if self.options.import_source != "react" {
|
||||
self.ctx.error(ImportSourceCannotBeSet);
|
||||
}
|
||||
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
program.body.insert(0, self.jsx_source.get_var_file_name_statement());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -118,11 +127,21 @@ impl<'a> ReactJsx<'a> {
|
|||
}
|
||||
|
||||
let imports = self.ctx.module_imports.get_import_statements();
|
||||
let index = program
|
||||
let mut index = program
|
||||
.body
|
||||
.iter()
|
||||
.rposition(|stmt| matches!(stmt, Statement::ImportDeclaration(_)))
|
||||
.map_or(0, |i| i + 1);
|
||||
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
program.body.insert(index, self.jsx_source.get_var_file_name_statement());
|
||||
// If source type is module then we need to add the import statement after the var file name statement
|
||||
// Follow the same behavior as babel
|
||||
if !self.is_script() {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
program.body.splice(index..index, imports);
|
||||
}
|
||||
|
||||
|
|
@ -153,17 +172,17 @@ impl<'a> ReactJsx<'a> {
|
|||
fn add_require_jsx_runtime(&mut self) {
|
||||
if !self.require_jsx_runtime {
|
||||
self.require_jsx_runtime = true;
|
||||
self.add_require_statement(
|
||||
"_reactJsxRuntime",
|
||||
self.jsx_runtime_importer.clone(),
|
||||
false,
|
||||
);
|
||||
let variable_name =
|
||||
if self.options.development { "_reactJsxDevRuntime" } else { "_reactJsxRuntime" };
|
||||
self.add_require_statement(variable_name, self.jsx_runtime_importer.clone(), false);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_import_jsx(&mut self) {
|
||||
if self.is_script() {
|
||||
self.add_require_jsx_runtime();
|
||||
} else if self.options.development {
|
||||
self.add_import_jsx_dev();
|
||||
} else if !self.import_jsx {
|
||||
self.import_jsx = true;
|
||||
self.add_import_statement("jsx", "_jsx", self.jsx_runtime_importer.clone());
|
||||
|
|
@ -173,12 +192,23 @@ impl<'a> ReactJsx<'a> {
|
|||
fn add_import_jsxs(&mut self) {
|
||||
if self.is_script() {
|
||||
self.add_require_jsx_runtime();
|
||||
} else if self.options.development {
|
||||
self.add_import_jsx_dev();
|
||||
} else if !self.import_jsxs {
|
||||
self.import_jsxs = true;
|
||||
self.add_import_statement("jsxs", "_jsxs", self.jsx_runtime_importer.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn add_import_jsx_dev(&mut self) {
|
||||
if self.is_script() {
|
||||
self.add_require_jsx_runtime();
|
||||
} else if !self.import_jsx {
|
||||
self.import_jsx = true;
|
||||
self.add_import_statement("jsxDEV", "_jsxDEV", self.jsx_runtime_importer.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn add_import_fragment(&mut self) {
|
||||
if self.is_script() {
|
||||
self.add_require_jsx_runtime();
|
||||
|
|
@ -241,6 +271,10 @@ impl<'a, 'b> JSXElementOrFragment<'a, 'b> {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_fragment(&self) -> bool {
|
||||
matches!(self, Self::Fragment(_))
|
||||
}
|
||||
|
||||
/// The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread
|
||||
/// <https://github.com/microsoft/TypeScript/blob/6134091642f57c32f50e7b5604635e4d37dd19e8/src/compiler/transformers/jsx.ts#L264-L278>
|
||||
fn has_key_after_props_spread(&self) -> bool {
|
||||
|
|
@ -259,10 +293,32 @@ impl<'a, 'b> JSXElementOrFragment<'a, 'b> {
|
|||
|
||||
// Transform jsx
|
||||
impl<'a> ReactJsx<'a> {
|
||||
/// ## Automatic
|
||||
/// ### Element
|
||||
/// Builds JSX into:
|
||||
/// - Production: React.jsx(type, arguments, key)
|
||||
/// - Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self)
|
||||
///
|
||||
/// ### Fragment
|
||||
/// Builds JSX Fragment <></> into
|
||||
/// - Production: React.jsx(type, arguments)
|
||||
/// - Development: React.jsxDEV(type, { children })
|
||||
///
|
||||
/// ## Classic
|
||||
/// ### Element
|
||||
/// - Production: React.createElement(type, arguments, children)
|
||||
/// - Development: React.createElement(type, arguments, children, source, self)
|
||||
///
|
||||
/// ### Fragment
|
||||
/// React.createElement(React.Fragment, null, ...children)
|
||||
///
|
||||
fn transform_jsx<'b>(&mut self, e: &JSXElementOrFragment<'a, 'b>) -> Expression<'a> {
|
||||
let is_classic = self.default_runtime.is_classic();
|
||||
let is_automatic = self.default_runtime.is_automatic();
|
||||
let is_fragment = e.is_fragment();
|
||||
let has_key_after_props_spread = e.has_key_after_props_spread();
|
||||
// If has_key_after_props_spread is true, we need to fallback to `createElement` same behavior as classic runtime
|
||||
let is_classic = self.options.runtime.is_classic() || has_key_after_props_spread;
|
||||
let is_automatic = !is_classic;
|
||||
let is_development = self.options.development;
|
||||
|
||||
let mut arguments = self.ast().new_vec();
|
||||
arguments.push(Argument::from(match e {
|
||||
|
|
@ -278,12 +334,6 @@ impl<'a> ReactJsx<'a> {
|
|||
let attributes = e.attributes();
|
||||
let attributes_len = attributes.map_or(0, |attrs| attrs.len());
|
||||
|
||||
// Add `null` to second argument in classic mode
|
||||
if is_classic && attributes_len == 0 {
|
||||
let null_expr = self.ast().literal_null_expression(NullLiteral::new(SPAN));
|
||||
arguments.push(Argument::from(null_expr));
|
||||
}
|
||||
|
||||
// The object properties for the second argument of `React.createElement`
|
||||
let mut properties = self.ast().new_vec();
|
||||
|
||||
|
|
@ -317,7 +367,7 @@ impl<'a> ReactJsx<'a> {
|
|||
}
|
||||
// In automatic mode, extract the key before spread prop,
|
||||
// and add it to the third argument later.
|
||||
if is_automatic && !has_key_after_props_spread {
|
||||
if is_automatic {
|
||||
key_prop = attr.value.as_ref();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -364,35 +414,83 @@ impl<'a> ReactJsx<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
if self.options.is_jsx_self_plugin_enabled() {
|
||||
if let Some(span) = self_attr_span {
|
||||
self.jsx_self.report_error(span);
|
||||
} else {
|
||||
properties.push(self.jsx_self.get_object_property_kind_for_jsx_plugin());
|
||||
// React.createElement's second argument
|
||||
if !is_fragment && is_classic {
|
||||
if self.options.is_jsx_self_plugin_enabled() {
|
||||
if let Some(span) = self_attr_span {
|
||||
self.jsx_self.report_error(span);
|
||||
} else {
|
||||
properties.push(self.jsx_self.get_object_property_kind_for_jsx_plugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
if let Some(span) = source_attr_span {
|
||||
self.jsx_source.report_error(span);
|
||||
} else {
|
||||
let (line, column) = get_line_column(e.span().start, self.ctx.source_text);
|
||||
properties
|
||||
.push(self.jsx_source.get_object_property_kind_for_jsx_plugin(line, column));
|
||||
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
if let Some(span) = source_attr_span {
|
||||
self.jsx_source.report_error(span);
|
||||
} else {
|
||||
let (line, column) = get_line_column(e.span().start, self.ctx.source_text);
|
||||
properties.push(
|
||||
self.jsx_source.get_object_property_kind_for_jsx_plugin(line, column),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.add_import(e, has_key_after_props_spread, need_jsxs);
|
||||
|
||||
if !properties.is_empty() || is_automatic {
|
||||
// If runtime is automatic that means we always to add `{ .. }` as the second argument even if it's empty
|
||||
if is_automatic || !properties.is_empty() {
|
||||
let object_expression = self.ast().object_expression(SPAN, properties, None);
|
||||
arguments.push(Argument::from(object_expression));
|
||||
} else if arguments.len() == 1 {
|
||||
// If not and second argument doesn't exist, we should add `null` as the second argument
|
||||
let null_expr = self.ast().literal_null_expression(NullLiteral::new(SPAN));
|
||||
arguments.push(Argument::from(null_expr));
|
||||
}
|
||||
|
||||
if is_automatic && key_prop.is_some() {
|
||||
arguments.push(Argument::from(self.transform_jsx_attribute_value(key_prop)));
|
||||
}
|
||||
// Only jsx and jsxDev will have more than 2 arguments
|
||||
if is_automatic {
|
||||
// key
|
||||
if key_prop.is_some() {
|
||||
arguments.push(Argument::from(self.transform_jsx_attribute_value(key_prop)));
|
||||
} else if is_development {
|
||||
arguments.push(Argument::from(self.ctx.ast.void_0()));
|
||||
}
|
||||
|
||||
if is_classic && !children.is_empty() {
|
||||
// isStaticChildren
|
||||
if is_development {
|
||||
let literal = self
|
||||
.ctx
|
||||
.ast
|
||||
.boolean_literal(SPAN, if is_fragment { false } else { children.len() > 1 });
|
||||
arguments.push(Argument::from(self.ctx.ast.literal_boolean_expression(literal)));
|
||||
}
|
||||
|
||||
// Fragment doesn't have source and self
|
||||
if !is_fragment {
|
||||
// { __source: { fileName, lineNumber, columnNumber } }
|
||||
if self.options.is_jsx_source_plugin_enabled() {
|
||||
if let Some(span) = source_attr_span {
|
||||
self.jsx_source.report_error(span);
|
||||
} else {
|
||||
let (line, column) = get_line_column(e.span().start, self.ctx.source_text);
|
||||
let expr = self.jsx_source.get_source_object(line, column);
|
||||
arguments.push(Argument::from(expr));
|
||||
}
|
||||
}
|
||||
|
||||
// this
|
||||
if self.options.is_jsx_self_plugin_enabled() {
|
||||
if let Some(span) = self_attr_span {
|
||||
self.jsx_self.report_error(span);
|
||||
} else {
|
||||
arguments.push(Argument::from(self.ctx.ast.this_expression(SPAN)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// React.createElement(type, arguments, ...children)
|
||||
// ^^^^^^^^^^^
|
||||
arguments.extend(
|
||||
children
|
||||
.iter()
|
||||
|
|
@ -445,7 +543,12 @@ impl<'a> ReactJsx<'a> {
|
|||
}
|
||||
ReactJsxRuntime::Automatic => {
|
||||
if self.is_script() {
|
||||
self.get_static_member_expression("_reactJsxRuntime", "Fragment")
|
||||
let object_name = if self.options.development {
|
||||
"_reactJsxDevRuntime"
|
||||
} else {
|
||||
"_reactJsxRuntime"
|
||||
};
|
||||
self.get_static_member_expression(object_name, "Fragment")
|
||||
} else {
|
||||
let ident = IdentifierReference::new(SPAN, "_Fragment".into());
|
||||
self.ast().identifier_reference_expression(ident)
|
||||
|
|
@ -469,6 +572,8 @@ impl<'a> ReactJsx<'a> {
|
|||
let name = if self.is_script() {
|
||||
if has_key_after_props_spread {
|
||||
"createElement"
|
||||
} else if self.options.development {
|
||||
"jsxDEV"
|
||||
} else if jsxs {
|
||||
"jsxs"
|
||||
} else {
|
||||
|
|
@ -476,14 +581,21 @@ impl<'a> ReactJsx<'a> {
|
|||
}
|
||||
} else if has_key_after_props_spread {
|
||||
"_createElement"
|
||||
} else if self.options.development {
|
||||
"_jsxDEV"
|
||||
} else if jsxs {
|
||||
"_jsxs"
|
||||
} else {
|
||||
"_jsx"
|
||||
};
|
||||
if self.is_script() {
|
||||
let object_ident_name =
|
||||
if has_key_after_props_spread { "_react" } else { "_reactJsxRuntime" };
|
||||
let object_ident_name = if has_key_after_props_spread {
|
||||
"_react"
|
||||
} else if self.options.development {
|
||||
"_reactJsxDevRuntime"
|
||||
} else {
|
||||
"_reactJsxRuntime"
|
||||
};
|
||||
self.get_static_member_expression(object_ident_name, name)
|
||||
} else {
|
||||
let ident = IdentifierReference::new(SPAN, name.into());
|
||||
|
|
|
|||
|
|
@ -27,22 +27,11 @@ const FILE_NAME_VAR: &str = "_jsxFileName";
|
|||
/// 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), should_add_jsx_file_name_variable: false }
|
||||
}
|
||||
|
||||
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);
|
||||
Self { ctx: Rc::clone(ctx) }
|
||||
}
|
||||
|
||||
pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
|
||||
|
|
@ -54,7 +43,6 @@ impl<'a> ReactJsxSource<'a> {
|
|||
line: usize,
|
||||
column: usize,
|
||||
) -> 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);
|
||||
|
|
@ -84,8 +72,6 @@ impl<'a> ReactJsxSource<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
self.should_add_jsx_file_name_variable = true;
|
||||
|
||||
let key = JSXAttributeName::Identifier(
|
||||
self.ctx.ast.alloc(self.ctx.ast.jsx_identifier(SPAN, SOURCE.into())),
|
||||
);
|
||||
|
|
@ -98,7 +84,7 @@ impl<'a> ReactJsxSource<'a> {
|
|||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn get_source_object(&self, line: usize, column: usize) -> Expression<'a> {
|
||||
pub fn get_source_object(&mut self, line: usize, column: usize) -> Expression<'a> {
|
||||
let kind = PropertyKind::Init;
|
||||
|
||||
let filename = {
|
||||
|
|
@ -142,7 +128,7 @@ impl<'a> ReactJsxSource<'a> {
|
|||
self.ctx.ast.object_expression(SPAN, properties, None)
|
||||
}
|
||||
|
||||
fn get_var_file_name_statement(&self) -> Statement<'a> {
|
||||
pub 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());
|
||||
|
|
|
|||
|
|
@ -47,14 +47,9 @@ 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.is_jsx_plugin_enabled() {
|
||||
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>) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
Passed: 296/362
|
||||
Passed: 303/362
|
||||
|
||||
# All Passed:
|
||||
* babel-preset-react
|
||||
* babel-plugin-transform-react-display-name
|
||||
* babel-plugin-transform-react-jsx-source
|
||||
|
||||
|
|
@ -62,9 +63,6 @@ Passed: 296/362
|
|||
* optimize-const-enums/merged-exported/input.ts
|
||||
* regression/15768/input.ts
|
||||
|
||||
# babel-preset-react (8/9)
|
||||
* preset-options/development-runtime-automatic/input.js
|
||||
|
||||
# babel-plugin-transform-react-jsx (141/143)
|
||||
* autoImport/complicated-scope-module/input.js
|
||||
* react-automatic/should-throw-when-filter-is-specified/input.js
|
||||
|
|
@ -73,13 +71,7 @@ Passed: 296/362
|
|||
* react-source/arrow-function/input.js
|
||||
* react-source/disable-with-super/input.js
|
||||
|
||||
# babel-plugin-transform-react-jsx-development (2/12)
|
||||
* cross-platform/auto-import-dev/input.js
|
||||
* cross-platform/classic-runtime/input.js
|
||||
* cross-platform/fragments/input.js
|
||||
* cross-platform/handle-fragments-with-key/input.js
|
||||
* cross-platform/handle-nonstatic-children/input.js
|
||||
* cross-platform/handle-static-children/input.js
|
||||
# babel-plugin-transform-react-jsx-development (8/12)
|
||||
* cross-platform/self-inside-arrow/input.mjs
|
||||
* cross-platform/source-and-self-defined/input.js
|
||||
* cross-platform/within-derived-classes-constructor/input.js
|
||||
|
|
|
|||
|
|
@ -83,9 +83,14 @@ fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOpti
|
|||
get_options::<ReactOptions>(options)?
|
||||
} else {
|
||||
let jsx_plugin = options.get_plugin("transform-react-jsx");
|
||||
let has_jsx_plugin = jsx_plugin.as_ref().is_some();
|
||||
let mut react_options =
|
||||
jsx_plugin.map(get_options::<ReactOptions>).transpose()?.unwrap_or_default();
|
||||
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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue