Skip to content

Commit

Permalink
feat(transformer/react): support development mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing committed May 10, 2024
1 parent 0ba7778 commit c41018b
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 76 deletions.
192 changes: 152 additions & 40 deletions crates/oxc_transformer/src/react/jsx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -75,7 +80,6 @@ impl<'a> ReactJsx<'a> {
import_jsxs: false,
import_fragment: false,
import_create_element: false,
default_runtime,
}
}

Expand Down Expand Up @@ -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;
}

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

Expand Down Expand Up @@ -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());
Expand All @@ -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();
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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();

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -469,21 +572,30 @@ 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 {
"jsx"
}
} 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());
Expand Down
20 changes: 3 additions & 17 deletions crates/oxc_transformer/src/react/jsx_source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>) {
Expand All @@ -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);
Expand Down Expand Up @@ -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())),
);
Expand All @@ -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 = {
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit c41018b

Please sign in to comment.