feat(transformer/react): support development mode (#3143)

This commit is contained in:
Dunqing 2024-05-10 22:07:33 +08:00 committed by GitHub
parent f0cbbbe28c
commit 18d853bb2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 166 additions and 76 deletions

View file

@ -45,7 +45,6 @@ pub struct ReactJsx<'a> {
// States // States
require_jsx_runtime: bool, require_jsx_runtime: bool,
jsx_runtime_importer: CompactStr, jsx_runtime_importer: CompactStr,
default_runtime: ReactJsxRuntime,
import_jsx: bool, import_jsx: bool,
import_jsxs: bool, import_jsxs: bool,
@ -59,9 +58,15 @@ impl<'a> ReactJsx<'a> {
let default_runtime = options.runtime; let default_runtime = options.runtime;
let jsx_runtime_importer = let jsx_runtime_importer =
if options.import_source == "react" || default_runtime.is_classic() { 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 { } else {
CompactStr::from(format!("{}/jsx-runtime", options.import_source)) CompactStr::from(format!(
"{}/jsx-{}runtime",
options.import_source,
if options.development { "dev-" } else { "" }
))
}; };
Self { Self {
@ -75,7 +80,6 @@ impl<'a> ReactJsx<'a> {
import_jsxs: false, import_jsxs: false,
import_fragment: false, import_fragment: false,
import_create_element: false, import_create_element: false,
default_runtime,
} }
} }
@ -107,6 +111,11 @@ impl<'a> ReactJsx<'a> {
if self.options.import_source != "react" { if self.options.import_source != "react" {
self.ctx.error(ImportSourceCannotBeSet); 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; return;
} }
@ -118,11 +127,21 @@ impl<'a> ReactJsx<'a> {
} }
let imports = self.ctx.module_imports.get_import_statements(); let imports = self.ctx.module_imports.get_import_statements();
let index = program let mut index = program
.body .body
.iter() .iter()
.rposition(|stmt| matches!(stmt, Statement::ImportDeclaration(_))) .rposition(|stmt| matches!(stmt, Statement::ImportDeclaration(_)))
.map_or(0, |i| i + 1); .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); program.body.splice(index..index, imports);
} }
@ -153,17 +172,17 @@ impl<'a> ReactJsx<'a> {
fn add_require_jsx_runtime(&mut self) { fn add_require_jsx_runtime(&mut self) {
if !self.require_jsx_runtime { if !self.require_jsx_runtime {
self.require_jsx_runtime = true; self.require_jsx_runtime = true;
self.add_require_statement( let variable_name =
"_reactJsxRuntime", if self.options.development { "_reactJsxDevRuntime" } else { "_reactJsxRuntime" };
self.jsx_runtime_importer.clone(), self.add_require_statement(variable_name, self.jsx_runtime_importer.clone(), false);
false,
);
} }
} }
fn add_import_jsx(&mut self) { fn add_import_jsx(&mut self) {
if self.is_script() { if self.is_script() {
self.add_require_jsx_runtime(); self.add_require_jsx_runtime();
} else if self.options.development {
self.add_import_jsx_dev();
} else if !self.import_jsx { } else if !self.import_jsx {
self.import_jsx = true; self.import_jsx = true;
self.add_import_statement("jsx", "_jsx", self.jsx_runtime_importer.clone()); 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) { fn add_import_jsxs(&mut self) {
if self.is_script() { if self.is_script() {
self.add_require_jsx_runtime(); self.add_require_jsx_runtime();
} else if self.options.development {
self.add_import_jsx_dev();
} else if !self.import_jsxs { } else if !self.import_jsxs {
self.import_jsxs = true; self.import_jsxs = true;
self.add_import_statement("jsxs", "_jsxs", self.jsx_runtime_importer.clone()); 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) { fn add_import_fragment(&mut self) {
if self.is_script() { if self.is_script() {
self.add_require_jsx_runtime(); 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 /// 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> /// <https://github.com/microsoft/TypeScript/blob/6134091642f57c32f50e7b5604635e4d37dd19e8/src/compiler/transformers/jsx.ts#L264-L278>
fn has_key_after_props_spread(&self) -> bool { fn has_key_after_props_spread(&self) -> bool {
@ -259,10 +293,32 @@ impl<'a, 'b> JSXElementOrFragment<'a, 'b> {
// Transform jsx // Transform jsx
impl<'a> ReactJsx<'a> { 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> { fn transform_jsx<'b>(&mut self, e: &JSXElementOrFragment<'a, 'b>) -> Expression<'a> {
let is_classic = self.default_runtime.is_classic(); let is_fragment = e.is_fragment();
let is_automatic = self.default_runtime.is_automatic();
let has_key_after_props_spread = e.has_key_after_props_spread(); 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(); let mut arguments = self.ast().new_vec();
arguments.push(Argument::from(match e { arguments.push(Argument::from(match e {
@ -278,12 +334,6 @@ impl<'a> ReactJsx<'a> {
let attributes = e.attributes(); let attributes = e.attributes();
let attributes_len = attributes.map_or(0, |attrs| attrs.len()); 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` // The object properties for the second argument of `React.createElement`
let mut properties = self.ast().new_vec(); let mut properties = self.ast().new_vec();
@ -317,7 +367,7 @@ impl<'a> ReactJsx<'a> {
} }
// In automatic mode, extract the key before spread prop, // In automatic mode, extract the key before spread prop,
// and add it to the third argument later. // 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(); key_prop = attr.value.as_ref();
continue; continue;
} }
@ -364,35 +414,83 @@ impl<'a> ReactJsx<'a> {
} }
} }
if self.options.is_jsx_self_plugin_enabled() { // React.createElement's second argument
if let Some(span) = self_attr_span { if !is_fragment && is_classic {
self.jsx_self.report_error(span); if self.options.is_jsx_self_plugin_enabled() {
} else { if let Some(span) = self_attr_span {
properties.push(self.jsx_self.get_object_property_kind_for_jsx_plugin()); 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 self.options.is_jsx_source_plugin_enabled() {
if let Some(span) = source_attr_span { if let Some(span) = source_attr_span {
self.jsx_source.report_error(span); self.jsx_source.report_error(span);
} else { } else {
let (line, column) = get_line_column(e.span().start, self.ctx.source_text); let (line, column) = get_line_column(e.span().start, self.ctx.source_text);
properties properties.push(
.push(self.jsx_source.get_object_property_kind_for_jsx_plugin(line, column)); self.jsx_source.get_object_property_kind_for_jsx_plugin(line, column),
);
}
} }
} }
self.add_import(e, has_key_after_props_spread, need_jsxs); 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); let object_expression = self.ast().object_expression(SPAN, properties, None);
arguments.push(Argument::from(object_expression)); 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() { // Only jsx and jsxDev will have more than 2 arguments
arguments.push(Argument::from(self.transform_jsx_attribute_value(key_prop))); 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( arguments.extend(
children children
.iter() .iter()
@ -445,7 +543,12 @@ impl<'a> ReactJsx<'a> {
} }
ReactJsxRuntime::Automatic => { ReactJsxRuntime::Automatic => {
if self.is_script() { 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 { } else {
let ident = IdentifierReference::new(SPAN, "_Fragment".into()); let ident = IdentifierReference::new(SPAN, "_Fragment".into());
self.ast().identifier_reference_expression(ident) self.ast().identifier_reference_expression(ident)
@ -469,6 +572,8 @@ impl<'a> ReactJsx<'a> {
let name = if self.is_script() { let name = if self.is_script() {
if has_key_after_props_spread { if has_key_after_props_spread {
"createElement" "createElement"
} else if self.options.development {
"jsxDEV"
} else if jsxs { } else if jsxs {
"jsxs" "jsxs"
} else { } else {
@ -476,14 +581,21 @@ impl<'a> ReactJsx<'a> {
} }
} else if has_key_after_props_spread { } else if has_key_after_props_spread {
"_createElement" "_createElement"
} else if self.options.development {
"_jsxDEV"
} else if jsxs { } else if jsxs {
"_jsxs" "_jsxs"
} else { } else {
"_jsx" "_jsx"
}; };
if self.is_script() { if self.is_script() {
let object_ident_name = let object_ident_name = if has_key_after_props_spread {
if has_key_after_props_spread { "_react" } else { "_reactJsxRuntime" }; "_react"
} else if self.options.development {
"_reactJsxDevRuntime"
} else {
"_reactJsxRuntime"
};
self.get_static_member_expression(object_ident_name, name) self.get_static_member_expression(object_ident_name, name)
} else { } else {
let ident = IdentifierReference::new(SPAN, name.into()); let ident = IdentifierReference::new(SPAN, name.into());

View file

@ -27,22 +27,11 @@ const FILE_NAME_VAR: &str = "_jsxFileName";
/// TODO: get lineNumber and columnNumber from somewhere /// TODO: get lineNumber and columnNumber from somewhere
pub struct ReactJsxSource<'a> { pub struct ReactJsxSource<'a> {
ctx: Ctx<'a>, ctx: Ctx<'a>,
/// Has `var _jsxFileName = "";` been added to program.statements?
should_add_jsx_file_name_variable: bool,
} }
impl<'a> ReactJsxSource<'a> { impl<'a> ReactJsxSource<'a> {
pub fn new(ctx: &Ctx<'a>) -> Self { pub fn new(ctx: &Ctx<'a>) -> Self {
Self { ctx: Rc::clone(ctx), should_add_jsx_file_name_variable: false } Self { ctx: Rc::clone(ctx) }
}
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>) { pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
@ -54,7 +43,6 @@ impl<'a> ReactJsxSource<'a> {
line: usize, line: usize,
column: usize, column: usize,
) -> ObjectPropertyKind<'a> { ) -> ObjectPropertyKind<'a> {
self.should_add_jsx_file_name_variable = true;
let kind = PropertyKind::Init; let kind = PropertyKind::Init;
let ident = IdentifierName::new(SPAN, SOURCE.into()); let ident = IdentifierName::new(SPAN, SOURCE.into());
let key = self.ctx.ast.property_key_identifier(ident); 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( let key = JSXAttributeName::Identifier(
self.ctx.ast.alloc(self.ctx.ast.jsx_identifier(SPAN, SOURCE.into())), 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)] #[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 kind = PropertyKind::Init;
let filename = { let filename = {
@ -142,7 +128,7 @@ impl<'a> ReactJsxSource<'a> {
self.ctx.ast.object_expression(SPAN, properties, None) 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 var_kind = VariableDeclarationKind::Var;
let id = { let id = {
let ident = BindingIdentifier::new(SPAN, FILE_NAME_VAR.into()); let ident = BindingIdentifier::new(SPAN, FILE_NAME_VAR.into());

View file

@ -47,14 +47,9 @@ impl<'a> React<'a> {
// Transforms // Transforms
impl<'a> React<'a> { impl<'a> React<'a> {
pub fn transform_program_on_exit(&mut self, program: &mut Program<'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() { if self.options.is_jsx_plugin_enabled() {
self.jsx.transform_program_on_exit(program); 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>) { pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {

View file

@ -1,6 +1,7 @@
Passed: 296/362 Passed: 303/362
# All Passed: # All Passed:
* babel-preset-react
* babel-plugin-transform-react-display-name * babel-plugin-transform-react-display-name
* babel-plugin-transform-react-jsx-source * babel-plugin-transform-react-jsx-source
@ -62,9 +63,6 @@ Passed: 296/362
* optimize-const-enums/merged-exported/input.ts * optimize-const-enums/merged-exported/input.ts
* regression/15768/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) # babel-plugin-transform-react-jsx (141/143)
* autoImport/complicated-scope-module/input.js * autoImport/complicated-scope-module/input.js
* react-automatic/should-throw-when-filter-is-specified/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/arrow-function/input.js
* react-source/disable-with-super/input.js * react-source/disable-with-super/input.js
# babel-plugin-transform-react-jsx-development (2/12) # babel-plugin-transform-react-jsx-development (8/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
* cross-platform/self-inside-arrow/input.mjs * cross-platform/self-inside-arrow/input.mjs
* cross-platform/source-and-self-defined/input.js * cross-platform/source-and-self-defined/input.js
* cross-platform/within-derived-classes-constructor/input.js * cross-platform/within-derived-classes-constructor/input.js

View file

@ -83,9 +83,14 @@ fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOpti
get_options::<ReactOptions>(options)? get_options::<ReactOptions>(options)?
} else { } else {
let jsx_plugin = options.get_plugin("transform-react-jsx"); let jsx_plugin = options.get_plugin("transform-react-jsx");
let has_jsx_plugin = jsx_plugin.as_ref().is_some(); let jsx_development_plugin = options.get_plugin("transform-react-jsx-development");
let mut react_options = let has_jsx_plugin =
jsx_plugin.map(get_options::<ReactOptions>).transpose()?.unwrap_or_default(); 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.development = options.get_plugin("transform-react-jsx-development").is_some();
react_options.jsx_plugin = has_jsx_plugin; react_options.jsx_plugin = has_jsx_plugin;
react_options.display_name_plugin = react_options.display_name_plugin =