From f1fcdde593f81d1007b04c8256d9c7fad7747d8f Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:41:30 +0000 Subject: [PATCH] feat(transformer): support react fast refresh (#4587) close: #3943 ## Further improvements There is a double visit here. We need to collect all react hooks calling in `Function` and `ArrowFunctionExpression`. If we want to remove this implementation, we may wait for #4188. https://github.com/oxc-project/oxc/blob/d797e595d286c613848b773c256bd43124ad1981/crates/oxc_transformer/src/react/refresh.rs#L744-L947 ## Tests All tests copy from https://github.com/facebook/react/blob/main/packages/react-refresh/src/__tests__/ReactFresh-test.js There are still 4 tests that have not been passed **1. refresh/can-handle-implicit-arrow-returns/input.jsx** Related to #4767. transform correct, just output doesn't match the expected output **2. refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx** **3. refresh/registers-identifiers-used-in-react-create-element-at-definition-site/input.jsx** Blocked by #4746 **4. refresh/supports-typescript-namespace-syntax/input.tsx** oxc transforms ts to js first, so probably we can ignore this case. If we really want to pass this test, we also need to turn off `TypeScript` plugin. ## What's next? ### Options: 1. Support transform `refresh_reg` and `refresh_sig` options to `MemberExpression`. Currently `import.meta.xxxx` still is an `Identifier` 2. Support `emit_full_signatures` option ### Other NAPI, testing in `monitor-oxc`, etc.. --- crates/oxc_transformer/src/lib.rs | 6 +- crates/oxc_transformer/src/react/mod.rs | 55 +- crates/oxc_transformer/src/react/options.rs | 39 + crates/oxc_transformer/src/react/refresh.rs | 1036 +++++++++++++++++ tasks/transform_conformance/oxc.snap.md | 9 +- .../input.jsx | 6 + .../output.js | 38 + .../input.jsx | 21 + .../output.js | 11 + .../does-not-get-tripped-by-iifes/input.jsx | 5 + .../does-not-get-tripped-by-iifes/output.js | 7 + .../input.jsx | 5 + .../output.js | 12 + .../input.jsx | 27 + .../output.js | 42 + .../input.jsx | 14 + .../output.js | 25 + .../ignores-complex-definitions/input.jsx | 12 + .../ignores-complex-definitions/output.js | 13 + .../input.jsx | 7 + .../output.js | 9 + .../refresh/ignores-hoc-definitions/input.jsx | 17 + .../refresh/ignores-hoc-definitions/output.js | 21 + .../input.jsx | 1 + .../output.js | 1 + .../input.jsx | 8 + .../options.json | 13 + .../output.js | 17 + .../input.jsx | 14 + .../output.js | 27 + .../input.jsx | 3 + .../output.js | 3 + .../test/fixtures/refresh/options.json | 5 + .../input.jsx | 7 + .../output.js | 15 + .../input.jsx | 30 + .../output.js | 32 + .../input.jsx | 40 + .../output.js | 32 + .../input.jsx | 9 + .../output.js | 21 + .../input.jsx | 3 + .../output.js | 8 + .../input.jsx | 3 + .../output.js | 8 + .../input.jsx | 18 + .../output.js | 27 + .../input.jsx | 12 + .../output.js | 17 + .../input.jsx | 8 + .../output.js | 23 + .../input.jsx | 10 + .../output.js | 20 + .../input.jsx | 12 + .../output.js | 19 + .../input.tsx | 15 + .../output.ts | 25 + .../input.jsx | 4 + .../options.json | 8 + .../output.js | 12 + .../input.jsx | 4 + .../output.js | 8 + 62 files changed, 1972 insertions(+), 7 deletions(-) create mode 100644 crates/oxc_transformer/src/react/refresh.rs create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/input.tsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/output.ts create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/output.js diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 43fc40dc02dff..47b332255aefb 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -45,7 +45,7 @@ pub use crate::{ env::EnvOptions, es2015::{ArrowFunctionsOptions, ES2015Options}, options::{BabelOptions, TransformOptions}, - react::{ReactJsxRuntime, ReactOptions}, + react::{ReactJsxRuntime, ReactOptions, ReactRefreshOptions}, typescript::TypeScriptOptions, }; use crate::{ @@ -127,6 +127,7 @@ impl<'a> Transformer<'a> { impl<'a> Traverse<'a> for Transformer<'a> { fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_program(program, ctx); + self.x1_react.transform_program(program, ctx); } fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { @@ -183,6 +184,7 @@ impl<'a> Traverse<'a> for Transformer<'a> { } fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.x1_react.transform_expression_on_exit(expr, ctx); self.x3_es2015.transform_expression_on_exit(expr, ctx); } @@ -265,6 +267,7 @@ impl<'a> Traverse<'a> for Transformer<'a> { fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_statements(stmts); + self.x1_react.transform_statements(stmts, ctx); self.x2_es2021.transform_statements(stmts, ctx); self.x2_es2020.transform_statements(stmts, ctx); self.x2_es2016.transform_statements(stmts, ctx); @@ -273,6 +276,7 @@ impl<'a> Traverse<'a> for Transformer<'a> { fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_statements_on_exit(stmts, ctx); + self.x1_react.transform_statements_on_exit(stmts, ctx); self.x2_es2021.transform_statements_on_exit(stmts, ctx); self.x2_es2020.transform_statements_on_exit(stmts, ctx); self.x2_es2016.transform_statements_on_exit(stmts, ctx); diff --git a/crates/oxc_transformer/src/react/mod.rs b/crates/oxc_transformer/src/react/mod.rs index ab9bd9c10f97e..35ec6f74afeed 100644 --- a/crates/oxc_transformer/src/react/mod.rs +++ b/crates/oxc_transformer/src/react/mod.rs @@ -4,17 +4,20 @@ mod jsx; mod jsx_self; mod jsx_source; mod options; +mod refresh; mod utils; use std::rc::Rc; +use oxc_allocator::Vec; use oxc_ast::ast::*; use oxc_traverse::TraverseCtx; +use refresh::ReactRefresh; pub use self::{ display_name::ReactDisplayName, jsx::ReactJsx, - options::{ReactJsxRuntime, ReactOptions}, + options::{ReactJsxRuntime, ReactOptions, ReactRefreshOptions}, }; use crate::context::Ctx; @@ -29,10 +32,12 @@ use crate::context::Ctx; pub struct React<'a> { jsx: ReactJsx<'a>, display_name: ReactDisplayName<'a>, + refresh: ReactRefresh<'a>, jsx_plugin: bool, display_name_plugin: bool, jsx_self_plugin: bool, jsx_source_plugin: bool, + refresh_plugin: bool, } // Constructors @@ -49,29 +54,61 @@ impl<'a> React<'a> { jsx_source_plugin, .. } = options; + let refresh = options.refresh.clone(); Self { jsx: ReactJsx::new(options, Rc::clone(&ctx)), - display_name: ReactDisplayName::new(ctx), + display_name: ReactDisplayName::new(Rc::clone(&ctx)), jsx_plugin, display_name_plugin, jsx_self_plugin, jsx_source_plugin, + refresh_plugin: refresh.is_some(), + refresh: ReactRefresh::new(&refresh.unwrap_or_default(), ctx), } } } // Transforms impl<'a> React<'a> { + pub fn transform_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.refresh_plugin { + self.refresh.transform_program(program, ctx); + } + } + pub fn transform_program_on_exit( &mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>, ) { + if self.refresh_plugin { + self.refresh.transform_program_on_exit(program, ctx); + } if self.jsx_plugin { self.jsx.transform_program_on_exit(program, ctx); } } + pub fn transform_statements( + &mut self, + stmts: &mut Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.refresh_plugin { + self.refresh.transform_statements(stmts, ctx); + } + } + + pub fn transform_statements_on_exit( + &mut self, + stmts: &mut Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.refresh_plugin { + self.refresh.transform_statements_on_exit(stmts, ctx); + } + } + pub fn transform_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { if self.jsx_plugin { match expr { @@ -87,9 +124,9 @@ impl<'a> React<'a> { } pub fn transform_call_expression( - &self, + &mut self, call_expr: &mut CallExpression<'a>, - ctx: &TraverseCtx<'a>, + ctx: &mut TraverseCtx<'a>, ) { if self.display_name_plugin { self.display_name.transform_call_expression(call_expr, ctx); @@ -108,4 +145,14 @@ impl<'a> React<'a> { self.jsx.jsx_source.transform_jsx_opening_element(elem, ctx); } } + + pub fn transform_expression_on_exit( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.refresh_plugin { + self.refresh.transform_expression_on_exit(expr, ctx); + } + } } diff --git a/crates/oxc_transformer/src/react/options.rs b/crates/oxc_transformer/src/react/options.rs index ab38da9647dde..5b9c590342d68 100644 --- a/crates/oxc_transformer/src/react/options.rs +++ b/crates/oxc_transformer/src/react/options.rs @@ -104,6 +104,9 @@ pub struct ReactOptions { /// /// This value is used to skip Babel tests, and is not used in oxc. pub use_spread: Option, + + /// Fast Refresh + pub refresh: Option, } impl Default for ReactOptions { @@ -122,6 +125,7 @@ impl Default for ReactOptions { pragma_frag: None, use_built_ins: None, use_spread: None, + refresh: None, } } } @@ -186,3 +190,38 @@ impl ReactOptions { } } } + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ReactRefreshOptions { + /// Specify the identifier of the refresh registration variable. + /// Defaults to `$RefreshReg$`. + #[serde(default = "default_refresh_reg")] + pub refresh_reg: String, + + /// Specify the identifier of the refresh signature variable. + /// Defaults to `$RefreshSig$`. + #[serde(default = "default_refresh_sig")] + pub refresh_sig: String, + + #[serde(default)] + pub emit_full_signatures: bool, +} + +impl Default for ReactRefreshOptions { + fn default() -> Self { + Self { + refresh_reg: default_refresh_reg(), + refresh_sig: default_refresh_sig(), + emit_full_signatures: false, + } + } +} + +fn default_refresh_reg() -> String { + String::from("$RefreshReg$") +} + +fn default_refresh_sig() -> String { + String::from("$RefreshSig$") +} diff --git a/crates/oxc_transformer/src/react/refresh.rs b/crates/oxc_transformer/src/react/refresh.rs new file mode 100644 index 0000000000000..1e025236ae834 --- /dev/null +++ b/crates/oxc_transformer/src/react/refresh.rs @@ -0,0 +1,1036 @@ +use std::cell::Cell; + +use oxc_allocator::CloneIn; +use oxc_ast::{ + ast::*, match_expression, match_member_expression, visit::walk::walk_variable_declarator, Visit, +}; +use oxc_semantic::{ReferenceFlag, ScopeFlags, ScopeId, SymbolFlags, SymbolId}; +use oxc_span::{Atom, GetSpan, Span, SPAN}; +use oxc_syntax::operator::AssignmentOperator; +use oxc_traverse::{Ancestor, TraverseCtx}; + +use super::options::ReactRefreshOptions; + +use crate::context::Ctx; + +/// React Fast Refresh +/// +/// Transform React functional components to integrate Fast Refresh. +/// +/// References: +/// +/// * +/// * +pub struct ReactRefresh<'a> { + refresh_reg: Atom<'a>, + refresh_sig: Atom<'a>, + _emit_full_signatures: bool, + registrations: Vec<(SymbolId, Atom<'a>)>, + ctx: Ctx<'a>, + signature_declarator_items: Vec>>, + /// Used to wrap call expression with signature. + /// (eg: hoc(() => {}) -> _s1(hoc(_s1(() => {})))) + last_signature: Option<(BindingIdentifier<'a>, oxc_allocator::Vec<'a, Argument<'a>>)>, +} + +impl<'a> ReactRefresh<'a> { + pub fn new(options: &ReactRefreshOptions, ctx: Ctx<'a>) -> Self { + // TODO: refresh_reg and refresh_sig need to support MemberExpression + Self { + refresh_reg: ctx.ast.atom(&options.refresh_reg), + refresh_sig: ctx.ast.atom(&options.refresh_sig), + _emit_full_signatures: options.emit_full_signatures, + signature_declarator_items: Vec::new(), + registrations: Vec::default(), + ctx, + last_signature: None, + } + } + + fn create_registration( + &mut self, + persistent_id: Atom<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> AssignmentTarget<'a> { + let symbol_id = ctx.generate_uid_in_root_scope("c", SymbolFlags::FunctionScopedVariable); + self.registrations.push((symbol_id, persistent_id)); + let name = ctx.ast.atom(ctx.symbols().get_name(symbol_id)); + let ident = ctx.create_reference_id(SPAN, name, Some(symbol_id), ReferenceFlag::Write); + let ident = ctx.ast.simple_assignment_target_from_identifier_reference(ident); + ctx.ast.assignment_target_simple(ident) + } + + /// Similar to the `findInnerComponents` function in `react-refresh/babel`. + fn replace_inner_components( + &mut self, + inferred_name: &str, + expr: &mut Expression<'a>, + is_variable_declarator: bool, + ctx: &mut TraverseCtx<'a>, + ) -> bool { + match expr { + Expression::Identifier(ref ident) => { + // For case like: + // export const Something = hoc(Foo) + // we don't want to wrap Foo inside the call. + // Instead we assume it's registered at definition. + return is_componentish_name(&ident.name); + } + Expression::FunctionExpression(_) => {} + Expression::ArrowFunctionExpression(arrow) => { + // Don't transform `() => () => {}` + if arrow + .get_expression() + .is_some_and(|expr| matches!(expr, Expression::ArrowFunctionExpression(_))) + { + return false; + } + } + Expression::CallExpression(ref mut call_expr) => { + let allowed_callee = matches!( + call_expr.callee, + Expression::Identifier(_) + | Expression::ComputedMemberExpression(_) + | Expression::StaticMemberExpression(_) + ); + + if allowed_callee { + let callee_span = call_expr.callee.span(); + + let Some(argument_expr) = + call_expr.arguments.first_mut().and_then(|e| e.as_expression_mut()) + else { + return false; + }; + + let found_inside = self.replace_inner_components( + format!( + "{}${}", + inferred_name, + callee_span.source_text(self.ctx.source_text) + ) + .as_str(), + argument_expr, + /* is_variable_declarator */ false, + ctx, + ); + + if !found_inside { + return false; + } + + // const Foo = hoc1(hoc2(() => {})) + // export default memo(React.forwardRef(function() {})) + if is_variable_declarator { + return true; + } + } else { + return false; + } + } + _ => { + return false; + } + } + + *expr = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + self.create_registration(ctx.ast.atom(inferred_name), ctx), + ctx.ast.move_expression(expr), + ); + + true + } + + /// Create an identifier reference from a binding identifier. + fn create_identifier_reference_from_binding_identifier( + id: &BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + ctx.ast.expression_from_identifier_reference(ctx.create_reference_id( + SPAN, + id.name.clone(), + id.symbol_id.get(), + ReferenceFlag::Read, + )) + } + + /// _c = id.name; + fn create_assignment_expression( + &mut self, + id: &BindingIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let left = self.create_registration(id.name.clone(), ctx); + let right = ctx.create_bound_reference_id( + SPAN, + id.name.clone(), + id.symbol_id.get().unwrap(), + ReferenceFlag::Read, + ); + let right = ctx.ast.expression_from_identifier_reference(right); + let expr = ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right); + ctx.ast.statement_expression(SPAN, expr) + } + + fn create_signature_call_expression( + &mut self, + scope_id: ScopeId, + body: &mut FunctionBody<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option<(BindingIdentifier<'a>, oxc_allocator::Vec<'a, Argument<'a>>)> { + let arguments = + CalculateSignatureKey::new(self.ctx.source_text, scope_id, ctx).calculate(body)?; + + let symbol_id = + ctx.generate_uid("s", ctx.current_scope_id(), SymbolFlags::FunctionScopedVariable); + + let symbol_name = ctx.ast.atom(ctx.symbols().get_name(symbol_id)); + + let binding_identifier = BindingIdentifier { + span: SPAN, + name: symbol_name.clone(), + symbol_id: Cell::new(Some(symbol_id)), + }; + + let sig_identifier_reference = ctx.create_reference_id( + SPAN, + self.refresh_sig.clone(), + Some(symbol_id), + ReferenceFlag::Read, + ); + + // _s(); + let call_expression = ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call( + SPAN, + ctx.ast.vec(), + Self::create_identifier_reference_from_binding_identifier(&binding_identifier, ctx), + Option::::None, + false, + ), + ); + + body.statements.insert(0, call_expression); + + // _s = refresh_sig(); + self.signature_declarator_items.last_mut().unwrap().push(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.binding_pattern( + ctx.ast.binding_pattern_kind_from_binding_identifier(binding_identifier.clone()), + Option::::None, + false, + ), + Some(ctx.ast.expression_call( + SPAN, + ctx.ast.vec(), + ctx.ast.expression_from_identifier_reference(sig_identifier_reference.clone()), + Option::::None, + false, + )), + false, + )); + + // Following is the signature call expression, will be generated in call site. + // _s(App, signature_key, false, function() { return [] }); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ custom hooks only + Some((binding_identifier, arguments)) + } +} + +// Internal Methods for transforming +impl<'a> ReactRefresh<'a> { + /// Process statement and return a statement(if any) to insert it after current statement. + /// + /// ```js + /// const Foo = styled("div")`color: hotpink`; + /// function Bar() {} + /// ``` + /// to + /// ```js + /// const Foo = styled("div")`color: hotpink`; + /// _c = Foo; + /// function Bar() { } + /// _c1 = Bar; + /// ``` + fn process_statement( + &mut self, + statement: &mut Statement<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match statement { + Statement::VariableDeclaration(variable) => { + self.handle_variable_declaration(variable, ctx) + } + Statement::FunctionDeclaration(func) => self.handle_function_declaration(func, ctx), + Statement::ExportNamedDeclaration(export_decl) => { + if let Some(declaration) = &mut export_decl.declaration { + match declaration { + Declaration::FunctionDeclaration(func) => { + self.handle_function_declaration(func, ctx) + } + Declaration::VariableDeclaration(variable) => { + self.handle_variable_declaration(variable, ctx) + } + _ => None, + } + } else { + None + } + } + Statement::ExportDefaultDeclaration(ref mut stmt_decl) => { + match &mut stmt_decl.declaration { + declaration @ match_expression!(ExportDefaultDeclarationKind) => { + let expression = declaration.to_expression_mut(); + if !matches!(expression, Expression::CallExpression(_)) { + // For now, we only support possible HOC calls here. + // Named function declarations are handled in FunctionDeclaration. + // Anonymous direct exports like export default function() {} + // are currently ignored. + return None; + } + + // This code path handles nested cases like: + // export default memo(() => {}) + // In those cases it is more plausible people will omit names + // so they're worth handling despite possible false positives. + // More importantly, it handles the named case: + // export default memo(function Named() {}) + self.replace_inner_components( + "%default%", + expression, + /* is_variable_declarator */ false, + ctx, + ); + + None + } + ExportDefaultDeclarationKind::FunctionDeclaration(func) => { + if let Some(id) = &func.id { + if !is_componentish_name(&id.name) { + return None; + } + + return Some(self.create_assignment_expression(id, ctx)); + } + None + } + _ => None, + } + } + _ => None, + } + } + + fn handle_function_declaration( + &mut self, + func: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let Some(id) = &func.id else { + return None; + }; + + if !is_componentish_name(&id.name) { + return None; + } + + Some(self.create_assignment_expression(id, ctx)) + } + + fn handle_variable_declaration( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if decl.declarations.len() != 1 { + return None; + } + + let declarator = decl.declarations.first_mut().unwrap_or_else(|| unreachable!()); + let init = declarator.init.as_mut()?; + let id = declarator.id.get_binding_identifier()?; + + if !is_componentish_name(&id.name) { + return None; + } + + match init { + // Likely component definitions. + Expression::ArrowFunctionExpression(arrow) => { + // () => () => {} + if arrow.get_expression().is_some_and(|expr| matches!(expr, Expression::ArrowFunctionExpression(_))) { + return None; + } + } + Expression::FunctionExpression(_) + // Maybe something like styled.div`...` + | Expression::TaggedTemplateExpression(_) => { + // Special case when a variable would get an inferred name: + // let Foo = () => {} + // let Foo = function() {} + // let Foo = styled.div``; + // We'll register it on next line so that + // we don't mess up the inferred 'Foo' function name. + // (eg: with @babel/plugin-transform-react-display-name or + // babel-plugin-styled-components) + } + Expression::CallExpression(call_expr) => { + if matches!(call_expr.callee, Expression::ImportExpression(_)) + || call_expr.is_require_call() + { + return None; + } + + // Maybe a HOC. + // Try to determine if this is some form of import. + let found_inside = self.replace_inner_components( + &id.name, + init, + /* is_variable_declarator */ true, + ctx, + ); + if !found_inside { + return None; + } + + // See if this identifier is used in JSX. Then it's a component. + // TODO: + // https://github.com/facebook/react/blob/ba6a9e94edf0db3ad96432804f9931ce9dc89fec/packages/react-refresh/src/ReactFreshBabelPlugin.js#L161-L199 + } + _ => { + return None; + } + } + + Some(self.create_assignment_expression(id, ctx)) + } + + // --------------------------- refresh sig --------------------------- + + /// Modify a function to insert a signature call, + /// and return a statement to insert it after current statement + /// + /// ```js + /// function Foo() {}; + /// ``` + /// to + /// ```js + /// function Foo() { _s() }; _s(Foo, ...); + /// ``` + fn modify_function_for_signature( + &mut self, + func: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let id = func.id.as_ref()?; + let (binding_identifier, mut arguments) = + self.create_signature_call_expression(func.scope_id.get()?, func.body.as_mut()?, ctx)?; + + arguments.insert( + 0, + Argument::from(Self::create_identifier_reference_from_binding_identifier(id, ctx)), + ); + + Some(ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call( + SPAN, + arguments, + Self::create_identifier_reference_from_binding_identifier(&binding_identifier, ctx), + Option::::None, + false, + ), + )) + } + + /// Modify all variable declarations to insert a signature call, + /// and return a set of statements to insert it after current statement + /// + /// ```js + /// let Foo = () => {}; + /// let Foo = function() {}; + /// let Foo = styled.div`` + /// ``` + /// to + /// ```js + /// let Foo = () => {_s()}; _s(Foo, ...); + /// let Foo = function() {_s()}; _s(Foo, ...); + /// let Foo = styled.div``; _s(Foo, ...); + /// ``` + fn modify_variable_declaration_for_signature( + &mut self, + decl: &mut VariableDeclaration<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Vec> { + decl.declarations + .iter_mut() + .filter_map(|declarator| { + let id = declarator.id.get_binding_identifier()?; + let init = declarator.init.as_mut()?; + + let (scope_id, body) = match init { + Expression::FunctionExpression(func) => { + (func.scope_id.get(), func.body.as_mut()?) + } + Expression::ArrowFunctionExpression(arrow) => { + (arrow.scope_id.get(), &mut arrow.body) + } + _ => { + return None; + } + }; + + // Special case when a function would get an inferred name: + // let Foo = () => {} + // let Foo = function() {} + // We'll add signature it on next line so that + // we don't mess up the inferred 'Foo' function name. + + // Result: let Foo = () => {}; __signature(Foo, ...); + + let (binding_identifier, mut arguments) = + self.create_signature_call_expression(scope_id.unwrap(), body, ctx)?; + if let Expression::ArrowFunctionExpression(arrow) = init { + Self::transform_arrow_function_to_block(arrow, ctx); + } + + arguments.insert( + 0, + Argument::from(Self::create_identifier_reference_from_binding_identifier( + id, ctx, + )), + ); + + Some(ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call( + SPAN, + arguments, + Self::create_identifier_reference_from_binding_identifier( + &binding_identifier, + ctx, + ), + Option::::None, + false, + ), + )) + }) + .collect() + } + + /// Convert arrow function expression to normal arrow function + /// + /// ```js + /// () => 1 + /// ``` + /// to + /// ```js + /// () => { return 1 } + /// ``` + fn transform_arrow_function_to_block( + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !arrow.expression { + return; + } + + arrow.expression = false; + + let Some(Statement::ExpressionStatement(statement)) = arrow.body.statements.pop() else { + unreachable!("arrow function body is never empty") + }; + + arrow + .body + .statements + .push(ctx.ast.statement_return(SPAN, Some(statement.unbox().expression))); + } +} + +// Transform +impl<'a> ReactRefresh<'a> { + /// Mutate statements and insert new assignment statements; + pub fn transform_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + let mut new_statements = ctx.ast.vec_with_capacity(program.body.len()); + for mut statement in program.body.drain(..) { + let next_statement = self.process_statement(&mut statement, ctx); + new_statements.push(statement); + if let Some(assignment_expression) = next_statement { + new_statements.push(assignment_expression); + } + } + program.body = new_statements; + } + + /// Insert all registrations at the end of the program. + /// + /// ``` + /// _c1 = refresh_reg(Foo, ...); + /// _c2 = refresh_reg(Foo, ...); + /// ``` + pub fn transform_program_on_exit( + &mut self, + program: &mut Program<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.registrations.is_empty() { + return; + } + + let mut variable_declarator_items = ctx.ast.vec_with_capacity(self.registrations.len()); + let mut new_statements = ctx.ast.vec_with_capacity(self.registrations.len() + 1); + for (symbol_id, persistent_id) in self.registrations.drain(..) { + let name = ctx.ast.atom(ctx.symbols().get_name(symbol_id)); + let binding_identifier = BindingIdentifier { + name: name.clone(), + symbol_id: Cell::new(Some(symbol_id)), + span: SPAN, + }; + + variable_declarator_items.push( + ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + ctx.ast.binding_pattern( + ctx.ast.binding_pattern_kind_from_binding_identifier( + binding_identifier.clone(), + ), + None::>, + false, + ), + None, + false, + ), + ); + + let refresh_reg_ident = ctx.create_reference_id( + SPAN, + self.refresh_reg.clone(), + Some(symbol_id), + ReferenceFlag::Read, + ); + let callee = ctx.ast.expression_from_identifier_reference(refresh_reg_ident); + let mut arguments = ctx.ast.vec_with_capacity(2); + arguments.push(ctx.ast.argument_expression( + Self::create_identifier_reference_from_binding_identifier(&binding_identifier, ctx), + )); + arguments.push(ctx.ast.argument_expression( + ctx.ast.expression_string_literal(SPAN, self.ctx.ast.atom(&persistent_id)), + )); + new_statements.push(ctx.ast.statement_expression( + SPAN, + ctx.ast.expression_call( + SPAN, + arguments, + callee, + Option::::None, + false, + ), + )); + } + program.body.push(Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + variable_declarator_items, + false, + ))); + program.body.extend(new_statements); + } + + pub fn transform_statements( + &mut self, + _stmts: &mut oxc_allocator::Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.signature_declarator_items.push(ctx.ast.vec()); + } + + pub fn transform_statements_on_exit( + &mut self, + stmts: &mut oxc_allocator::Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + // TODO: check is there any function declaration + + let mut new_stmts = ctx.ast.vec_with_capacity(stmts.len() + 1); + + for mut stmt in stmts.drain(..) { + match &mut stmt { + Statement::FunctionDeclaration(func) => { + let call_signature_statement = self.modify_function_for_signature(func, ctx); + new_stmts.push(stmt); + new_stmts.extend(call_signature_statement); + continue; + } + Statement::VariableDeclaration(decl) => { + let call_signature_statements = + self.modify_variable_declaration_for_signature(decl, ctx); + new_stmts.push(stmt); + new_stmts.extend(call_signature_statements); + continue; + } + Statement::ExportNamedDeclaration(export_decl) => { + if let Some(Declaration::FunctionDeclaration(func)) = + &mut export_decl.declaration + { + let call_signature_statement = + self.modify_function_for_signature(func, ctx); + new_stmts.push(stmt); + new_stmts.extend(call_signature_statement); + continue; + } else if let Some(Declaration::VariableDeclaration(decl)) = + &mut export_decl.declaration + { + let call_signature_statements = + self.modify_variable_declaration_for_signature(decl, ctx); + new_stmts.push(stmt); + new_stmts.extend(call_signature_statements); + continue; + } + } + Statement::ExportDefaultDeclaration(export_decl) => { + if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = + &mut export_decl.declaration + { + if func.id.is_some() { + if let Some(bind_sig_statement) = + self.modify_function_for_signature(func, ctx) + { + new_stmts.push(stmt); + new_stmts.push(bind_sig_statement); + continue; + } + } + } + } + _ => {} + }; + new_stmts.push(stmt); + } + + let declarations = self.signature_declarator_items.pop().unwrap(); + if !declarations.is_empty() { + new_stmts.insert( + 0, + Statement::from(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + declarations, + false, + )), + ); + } + + *stmts = new_stmts; + } + + /// Transform an expression to insert a signature call, + /// and wrap it with a signature call + /// + /// ```js + /// Foo(() => {}) + /// let Foo = React.forwardRef(React.memo(() => {})); + /// ``` + /// to + /// ```js + /// Foo(_s1(() => {})) + /// let Foo = s1(React.forwardRef(_s1(React.memo(_s1(() => {_s1()}, ...), ...))), ...); + /// ``` + pub fn transform_expression_on_exit( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + let is_variable_declarator = matches!(ctx.parent(), Ancestor::VariableDeclaratorInit(_)); + + let signature = match expr { + Expression::FunctionExpression(func) if !is_variable_declarator => self + .create_signature_call_expression( + func.scope_id.get().unwrap(), + func.body.as_mut().unwrap(), + ctx, + ), + Expression::ArrowFunctionExpression(arrow) if !is_variable_declarator => { + let call_fn = self.create_signature_call_expression( + arrow.scope_id.get().unwrap(), + &mut arrow.body, + ctx, + ); + + // If the signature is found, we will push a new statement to the arrow function body. So it's not an expression anymore. + if call_fn.is_some() { + Self::transform_arrow_function_to_block(arrow, ctx); + } + call_fn + } + // hoc(_c = function() { }) + Expression::AssignmentExpression(_) => return, + // hoc1(hoc2(...)) + // Result: let Foo = __signature(hoc(__signature(() => {}, ...)), ...) + Expression::CallExpression(_) => self.last_signature.take(), + _ => None, + }; + + let Some(signature) = signature else { + return; + }; + + let mut found_call_expression = false; + for ancestor in ctx.ancestors() { + if ancestor.is_assignment_expression() { + continue; + } + if ancestor.is_call_expression() { + found_call_expression = true; + } + break; + } + + if found_call_expression { + self.last_signature = + Some((signature.0.clone(), signature.1.clone_in(ctx.ast.allocator))); + } + + let (binding_identifier, mut arguments) = signature; + arguments.insert(0, Argument::from(ctx.ast.move_expression(expr))); + *expr = self.ctx.ast.expression_call( + SPAN, + arguments, + Self::create_identifier_reference_from_binding_identifier(&binding_identifier, ctx), + Option::::None, + false, + ); + } +} + +// TODO: Try to remove this struct, avoid double visit +struct CalculateSignatureKey<'a, 'b> { + key: String, + source_text: &'a str, + ctx: &'b mut TraverseCtx<'a>, + callee_list: Vec<(Atom<'a>, Option>)>, + scope_ids: Vec, + declarator_id_span: Option, +} + +impl<'a, 'b> CalculateSignatureKey<'a, 'b> { + pub fn new(source_text: &'a str, scope_id: ScopeId, ctx: &'b mut TraverseCtx<'a>) -> Self { + Self { + key: String::new(), + ctx, + source_text, + scope_ids: vec![scope_id], + declarator_id_span: None, + callee_list: Vec::new(), + } + } + + fn current_scope_id(&self) -> ScopeId { + *self.scope_ids.last().unwrap() + } + + pub fn calculate( + mut self, + body: &FunctionBody<'a>, + ) -> Option>> { + for statement in &body.statements { + self.visit_statement(statement); + } + + if self.key.is_empty() { + return None; + } + + // Check if a corresponding binding exists where we emit the signature. + let mut force_reset = false; + let mut custom_hooks_in_scope = self.ctx.ast.vec_with_capacity(self.callee_list.len()); + + for (binding_name, hook_name) in &self.callee_list { + if let Some(symbol_id) = + self.ctx.scopes().find_binding(self.ctx.current_scope_id(), binding_name) + { + let ident = self.ctx.create_reference_id( + SPAN, + binding_name.clone(), + Some(symbol_id), + ReferenceFlag::Read, + ); + + let mut expr = self.ctx.ast.expression_from_identifier_reference(ident); + + if let Some(hook_name) = hook_name { + // binding_name.hook_name + expr = Expression::from(self.ctx.ast.member_expression_static( + SPAN, + expr, + self.ctx.ast.identifier_name(SPAN, hook_name), + false, + )); + } + + custom_hooks_in_scope.push(self.ctx.ast.array_expression_element_expression(expr)); + } else { + force_reset = true; + } + } + + let mut arguments = self.ctx.ast.vec_with_capacity( + 1 + usize::from(force_reset) + usize::from(!custom_hooks_in_scope.is_empty()), + ); + arguments.push(self.ctx.ast.argument_expression( + self.ctx.ast.expression_string_literal(SPAN, self.ctx.ast.atom(&self.key)), + )); + + if force_reset || !custom_hooks_in_scope.is_empty() { + arguments.push( + self.ctx.ast.argument_expression( + self.ctx.ast.expression_boolean_literal(SPAN, force_reset), + ), + ); + } + + if !custom_hooks_in_scope.is_empty() { + // function () { return custom_hooks_in_scope } + let formal_parameters = self.ctx.ast.formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + self.ctx.ast.vec(), + Option::::None, + ); + let function_body = self.ctx.ast.function_body( + SPAN, + self.ctx.ast.vec(), + self.ctx.ast.vec1(self.ctx.ast.statement_return( + SPAN, + Some(self.ctx.ast.expression_array(SPAN, custom_hooks_in_scope, None)), + )), + ); + let fn_expr = self.ctx.ast.expression_function( + FunctionType::FunctionExpression, + SPAN, + None, + false, + false, + false, + Option::::None, + Option::::None, + formal_parameters, + Option::::None, + Some(function_body), + ); + arguments.push(self.ctx.ast.argument_expression(fn_expr)); + } + + Some(arguments) + } +} + +impl<'a, 'b> Visit<'a> for CalculateSignatureKey<'a, 'b> { + fn enter_scope(&mut self, _flags: ScopeFlags, scope_id: &Cell>) { + self.scope_ids.push(scope_id.get().unwrap()); + } + + fn leave_scope(&mut self) { + self.scope_ids.pop(); + } + + fn visit_statements(&mut self, _stmt: &oxc_allocator::Vec<'a, Statement<'a>>) { + // We don't need calculate any signature in nested scopes + } + + fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) { + if matches!(declarator.init, Some(Expression::CallExpression(_))) { + self.declarator_id_span = Some(declarator.id.span()); + } + walk_variable_declarator(self, declarator); + // We doesn't check the call expression is the hook, + // So we need to reset the declarator_id_span after visiting the variable declarator. + self.declarator_id_span = None; + } + + fn visit_call_expression(&mut self, call_expr: &CallExpression<'a>) { + if !self.ctx.scopes().get_flags(self.current_scope_id()).is_function() { + return; + } + + let name = match &call_expr.callee { + Expression::Identifier(ident) => Some(ident.name.clone()), + Expression::StaticMemberExpression(ref member) => Some(member.property.name.clone()), + _ => None, + }; + + let Some(hook_name) = name else { + return; + }; + + if !is_use_hook_name(&hook_name) { + return; + } + + if !is_builtin_hook(&hook_name) { + let callee = match &call_expr.callee { + Expression::Identifier(ident) => Some((ident.name.clone(), None)), + callee @ match_member_expression!(Expression) => { + let member_expr = callee.to_member_expression(); + match member_expr.object() { + Expression::Identifier(ident) => { + Some((ident.name.clone(), Some(hook_name.clone()))) + } + _ => None, + } + } + _ => None, + }; + + if let Some(callee) = callee { + self.callee_list.push(callee); + } + } + + let args = &call_expr.arguments; + let args_key = if hook_name == "useState" && args.len() > 0 { + args[0].span().source_text(self.source_text) + } else if hook_name == "useReducer" && args.len() > 1 { + args[1].span().source_text(self.source_text) + } else { + "" + }; + + if !self.key.is_empty() { + self.key.push_str("\\n"); + } + self.key.push_str(&format!( + "{hook_name}{{{}{}{args_key}{}}}", + self.declarator_id_span.take().map_or("", |span| span.source_text(self.source_text)), + if args_key.is_empty() { "" } else { "(" }, + if args_key.is_empty() { "" } else { ")" } + )); + } +} + +fn is_componentish_name(name: &str) -> bool { + name.chars().next().unwrap().is_ascii_uppercase() +} + +fn is_use_hook_name(name: &str) -> bool { + name.starts_with("use") && name.chars().nth(3).unwrap().is_ascii_uppercase() +} + +#[rustfmt::skip] +fn is_builtin_hook(hook_name: &str) -> bool { + matches!( + hook_name, + "useState" | "useReducer" | "useEffect" | + "useLayoutEffect" | "useMemo" | "useCallback" | + "useRef" | "useContext" | "useImperativeHandle" | + "useDebugValue" | "useId" | "useDeferredValue" | + "useTransition" | "useInsertionEffect" | "useSyncExternalStore" | + "useFormStatus" | "useFormState" | "useActionState" | + "useOptimistic" + ) +} diff --git a/tasks/transform_conformance/oxc.snap.md b/tasks/transform_conformance/oxc.snap.md index ed5308934bf4f..f7e2a66f305e7 100644 --- a/tasks/transform_conformance/oxc.snap.md +++ b/tasks/transform_conformance/oxc.snap.md @@ -1,10 +1,9 @@ commit: 12619ffe -Passed: 6/9 +Passed: 28/35 # All Passed: * babel-plugin-transform-optional-catch-binding -* babel-plugin-transform-react-jsx # babel-plugin-transform-typescript (4/7) @@ -12,3 +11,9 @@ Passed: 6/9 * enum-member-reference/input.ts * export-elimination/input.ts +# babel-plugin-transform-react-jsx (23/27) +* refresh/can-handle-implicit-arrow-returns/input.jsx +* refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx +* refresh/registers-identifiers-used-in-react-create-element-at-definition-site/input.jsx +* refresh/supports-typescript-namespace-syntax/input.tsx + diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/input.jsx new file mode 100644 index 0000000000000..b45dbe7683553 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/input.jsx @@ -0,0 +1,6 @@ +export default () => useContext(X); +export const Foo = () => useContext(X); +module.exports = () => useContext(X); +const Bar = () => useContext(X); +const Baz = memo(() => useContext(X)); +const Qux = () => (0, useContext(X)); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/output.js new file mode 100644 index 0000000000000..51263c7e327b2 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/can-handle-implicit-arrow-returns/output.js @@ -0,0 +1,38 @@ +var _s = $RefreshSig$(), _s2 = $RefreshSig$(), _s3 = $RefreshSig$(), _s4 = $RefreshSig$(), _s5 = $RefreshSig$(), _s6 = $RefreshSig$(); +export default _s(() => { + _s(); + return useContext(X); +}, "useContext{}"); +export const Foo = () => { + _s2(); + return useContext(X); +}; +_s2(Foo, "useContext{}"); +_c = Foo; +module.exports = _s3(() => { + _s3(); + return useContext(X); +}, "useContext{}"); +const Bar = () => { + _s4(); + return useContext(X); +}; +_s4(Bar, "useContext{}"); +_c2 = Bar; +const Baz = _s5(memo(_c3 = _s5(() => { + _s5(); + return useContext(X); +}, "useContext{}")), "useContext{}"); +_c4 = Baz; +const Qux = () => { + _s6(); + return 0, useContext(X); +}; +_s6(Qux, "useContext{}"); +_c5 = Qux; +var _c, _c2, _c3, _c4, _c5; +$RefreshReg$(_c, "Foo"); +$RefreshReg$(_c2, "Bar"); +$RefreshReg$(_c3, "Baz$memo"); +$RefreshReg$(_c4, "Baz"); +$RefreshReg$(_c5, "Qux"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/input.jsx new file mode 100644 index 0000000000000..d55ce48f47911 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/input.jsx @@ -0,0 +1,21 @@ +// None of these were declared in this file. +// It's bad to register them because that would trigger +// modules to execute in an environment with inline requires. +// So we expect the transform to skip all of them even though +// they are used in JSX. + +const A = require('A'); +const B = foo ? require('X') : require('Y'); +const C = requireCond(gk, 'C'); +const D = import('D'); + +export default function App() { + return ( + + ); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/output.js new file mode 100644 index 0000000000000..e5b23f6644598 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-consider-require-like-methods-to-be-hocs/output.js @@ -0,0 +1,11 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +const A = require("A"); +const B = foo ? require("X") : require("Y"); +const C = requireCond(gk, "C"); +const D = import("D"); +export default function App() { + return _jsxs("div", { children: [_jsx(A, {}), _jsx(B, {}), _jsx(C, {}), _jsx(D, {})] }); +} +_c = App; +var _c; +$RefreshReg$(_c, "App"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/input.jsx new file mode 100644 index 0000000000000..089c664700653 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/input.jsx @@ -0,0 +1,5 @@ +while (item) { + (item => { + useFoo(); + })(item); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/output.js new file mode 100644 index 0000000000000..cb846ec609ffd --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/does-not-get-tripped-by-iifes/output.js @@ -0,0 +1,7 @@ +while (item) { + var _s = $RefreshSig$(); + _s((item) => { + _s(); + useFoo(); + }, "useFoo{}", true)(item); +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/input.jsx new file mode 100644 index 0000000000000..4a8b072c8b307 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/input.jsx @@ -0,0 +1,5 @@ +export default function App() { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/output.js new file mode 100644 index 0000000000000..c577ac792e5ba --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-declarations-calling-hooks/output.js @@ -0,0 +1,12 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +var _s = $RefreshSig$(); +export default function App() { + _s(); + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return _jsx("h1", { children: foo }); +} +_s(App, "useState{[foo, setFoo](0)}\\nuseEffect{}"); +_c = App; +var _c; +$RefreshReg$(_c, "App"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/input.jsx new file mode 100644 index 0000000000000..95c88048f02d1 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/input.jsx @@ -0,0 +1,27 @@ +// Unlike __register__, we want to sign all functions -- not just top level. +// This lets us support editing HOCs better. +// For function declarations, __signature__ is called on next line. +// For function expressions, it wraps the expression. +// In order for this to work, __signature__ returns its first argument. + +export const A = React.memo(React.forwardRef((props, ref) => { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; +})); + +export const B = React.memo(React.forwardRef(function(props, ref) { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; +})); + +function hoc() { + return function Inner() { + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return

{foo}

; + }; +} + +export let C = hoc(); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/output.js new file mode 100644 index 0000000000000..dba04069c6236 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-signatures-for-function-expressions-calling-hooks/output.js @@ -0,0 +1,42 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +var _s = $RefreshSig$(), _s2 = $RefreshSig$(); +export const A = _s(React.memo(_c2 = _s(React.forwardRef(_c = _s((props, ref) => { + _s(); + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return _jsx("h1", { + ref, + children: foo + }); +}, "useState{[foo, setFoo](0)}\\nuseEffect{}")), "useState{[foo, setFoo](0)}\\nuseEffect{}")), "useState{[foo, setFoo](0)}\\nuseEffect{}"); +_c3 = A; +export const B = _s2(React.memo(_c5 = _s2(React.forwardRef(_c4 = _s2(function(props, ref) { + _s2(); + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return _jsx("h1", { + ref, + children: foo + }); +}, "useState{[foo, setFoo](0)}\\nuseEffect{}")), "useState{[foo, setFoo](0)}\\nuseEffect{}")), "useState{[foo, setFoo](0)}\\nuseEffect{}"); +_c6 = B; +function hoc() { + var _s3 = $RefreshSig$(); + return _s3(function Inner() { + _s3(); + const [foo, setFoo] = useState(0); + React.useEffect(() => {}); + return _jsx("h1", { + ref, + children: foo + }); + }, "useState{[foo, setFoo](0)}\\nuseEffect{}"); +} +export let C = hoc(); +var _c, _c2, _c3, _c4, _c5, _c6; +$RefreshReg$(_c, "A$React.memo$React.forwardRef"); +$RefreshReg$(_c2, "A$React.memo"); +$RefreshReg$(_c3, "A"); +$RefreshReg$(_c4, "B$React.memo$React.forwardRef"); +$RefreshReg$(_c5, "B$React.memo"); +$RefreshReg$(_c6, "B"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/input.jsx new file mode 100644 index 0000000000000..ae402c002909b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/input.jsx @@ -0,0 +1,14 @@ +import FancyHook from 'fancy'; + +export default function App() { + function useFancyState() { + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + } + const bar = useFancyState(); + const baz = FancyHook.useThing(); + React.useState(); + useThePlatform(); + return

{bar}{baz}

; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/output.js new file mode 100644 index 0000000000000..343b414fe23d8 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/generates-valid-signature-for-exotic-ways-to-call-hooks/output.js @@ -0,0 +1,25 @@ +var _s2 = $RefreshSig$(); +import FancyHook from "fancy"; +import { jsxs as _jsxs } from "react/jsx-runtime"; +export default function App() { + _s2(); + var _s = $RefreshSig$(); + function useFancyState() { + _s(); + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; + } + _s(useFancyState, "useState{[foo, setFoo](0)}\\nuseFancyEffect{}", true); + const bar = useFancyState(); + const baz = FancyHook.useThing(); + React.useState(); + useThePlatform(); + return _jsxs("h1", { children: [bar, baz] }); +} +_s2(App, "useFancyState{bar}\\nuseThing{baz}\\nuseState{}\\nuseThePlatform{}", true, function() { + return [FancyHook.useThing]; +}); +_c = App; +var _c; +$RefreshReg$(_c, "App"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/input.jsx new file mode 100644 index 0000000000000..bd4daf1e61b18 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/input.jsx @@ -0,0 +1,12 @@ +let A = foo ? () => { + return

Hi

; +} : null +const B = (function Foo() { + return

Hi

; +})(); +let C = () => () => { + return

Hi

; +}; +let D = bar && (() => { + return

Hi

; +}); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/output.js new file mode 100644 index 0000000000000..bf9c28ed128d5 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-complex-definitions/output.js @@ -0,0 +1,13 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +let A = foo ? () => { + return _jsx("h1", { children: "Hi" }); +} : null; +const B = function Foo() { + return _jsx("h1", { children: "Hi" }); +}(); +let C = () => () => { + return _jsx("h1", { children: "Hi" }); +}; +let D = bar && (() => { + return _jsx("h1", { children: "Hi" }); +}); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/input.jsx new file mode 100644 index 0000000000000..1697a276ca95c --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/input.jsx @@ -0,0 +1,7 @@ +const throttledAlert = throttle(function() { + alert('Hi'); +}); +const TooComplex = (function() { return hello })(() => {}); +if (cond) { + const Foo = thing(() => {}); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/output.js new file mode 100644 index 0000000000000..2cda38d31ff9f --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-higher-order-functions-that-are-not-hocs/output.js @@ -0,0 +1,9 @@ +const throttledAlert = throttle(function() { + alert("Hi"); +}); +const TooComplex = function() { + return hello; +}(() => {}); +if (cond) { + const Foo = thing(() => {}); +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/input.jsx new file mode 100644 index 0000000000000..c08c8660ac999 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/input.jsx @@ -0,0 +1,17 @@ +// TODO: we might want to handle HOCs at usage site, however. +// TODO: it would be nice if we could always avoid registering +// a function that is known to return a function or other non-node. + +let connect = () => { + function Comp() { + const handleClick = () => {}; + return

Hi

; + } + return Comp; +}; +function withRouter() { + return function Child() { + const handleClick = () => {}; + return

Hi

; + } +}; \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/output.js new file mode 100644 index 0000000000000..e5af6de56291a --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-hoc-definitions/output.js @@ -0,0 +1,21 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +let connect = () => { + function Comp() { + const handleClick = () => {}; + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); + } + return Comp; +}; +function withRouter() { + return function Child() { + const handleClick = () => {}; + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); + }; +} +; diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/input.jsx new file mode 100644 index 0000000000000..421474fe3c0e7 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/input.jsx @@ -0,0 +1 @@ +export default function() {} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/output.js new file mode 100644 index 0000000000000..421474fe3c0e7 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/ignores-unnamed-function-declarations/output.js @@ -0,0 +1 @@ +export default function() {} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/input.jsx new file mode 100644 index 0000000000000..2adf1cfda72fe --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/input.jsx @@ -0,0 +1,8 @@ +// this test is passing with Babel 6 +// but would fail for Babel 7 _without_ custom hook node being cloned for signature +import {useFancyState} from './hooks'; + +export default function App() { + const bar = useFancyState(); + return

{bar}

; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/options.json new file mode 100644 index 0000000000000..41d36d77150ed --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/options.json @@ -0,0 +1,13 @@ +{ + "plugins": [ + [ + "transform-react-jsx", + { + "refresh": {} + } + ], + [ + "transform-modules-commonjs" + ] + ] +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/output.js new file mode 100644 index 0000000000000..ae43fecb626ee --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/output.js @@ -0,0 +1,17 @@ +"use strict"; +import { jsx as _jsx } from "react/jsx-runtime"; +Object.defineProperty(exports, "__esModule", { value: true }); +(exports.default = App); +var _hooks = require("./hooks"); +var _s = $RefreshSig$(); +function App() { + _s(); + const bar = (0, _hooks.useFancyState)(); + return _jsx("h1", { children: bar }); +} +_s(App, "useFancyState{bar}", false, function() { + return [_hooks.useFancyState]; +}); +_c = App; +var _c; +$RefreshReg$(_c, "App"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/input.jsx new file mode 100644 index 0000000000000..a7274ad7eef5e --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/input.jsx @@ -0,0 +1,14 @@ +function useFancyState() { + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; +} + +const useFancyEffect = () => { + React.useEffect(() => {}); +}; + +export default function App() { + const bar = useFancyState(); + return

{bar}

; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/output.js new file mode 100644 index 0000000000000..2742a9401bb2a --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/includes-custom-hooks-into-the-signatures/output.js @@ -0,0 +1,27 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +var _s = $RefreshSig$(), _s2 = $RefreshSig$(), _s3 = $RefreshSig$(); +function useFancyState() { + _s(); + const [foo, setFoo] = React.useState(0); + useFancyEffect(); + return foo; +} +_s(useFancyState, "useState{[foo, setFoo](0)}\\nuseFancyEffect{}", false, function() { + return [useFancyEffect]; +}); +const useFancyEffect = () => { + _s2(); + React.useEffect(() => {}); +}; +_s2(useFancyEffect, "useEffect{}"); +export default function App() { + _s3(); + const bar = useFancyState(); + return _jsx("h1", { children: bar }); +} +_s3(App, "useFancyState{bar}", false, function() { + return [useFancyState]; +}); +_c = App; +var _c; +$RefreshReg$(_c, "App"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/input.jsx new file mode 100644 index 0000000000000..5206a2ea76378 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/input.jsx @@ -0,0 +1,3 @@ +function hello() { + return 2 * 2; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/output.js new file mode 100644 index 0000000000000..5206a2ea76378 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/only-registers-pascal-case-functions/output.js @@ -0,0 +1,3 @@ +function hello() { + return 2 * 2; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/options.json new file mode 100644 index 0000000000000..ab1dae10ca5ae --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [["transform-react-jsx", { + "refresh": {} + }]] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/input.jsx new file mode 100644 index 0000000000000..7929fdb34ab5e --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/input.jsx @@ -0,0 +1,7 @@ +function Foo() { + return

Hi

; +} + +export default hoc(Foo); +export const A = hoc(Foo); +const B = hoc(Foo); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/output.js new file mode 100644 index 0000000000000..7ccb6117f5f30 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-capitalized-identifiers-in-hoc-calls/output.js @@ -0,0 +1,15 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +function Foo() { + return _jsx("h1", { children: "Hi" }); +} +_c = Foo; +export default _c2 = hoc(Foo); +export const A = hoc(Foo); +_c3 = A; +const B = hoc(Foo); +_c4 = B; +var _c, _c2, _c3, _c4; +$RefreshReg$(_c, "Foo"); +$RefreshReg$(_c2, "%default%"); +$RefreshReg$(_c3, "A"); +$RefreshReg$(_c4, "B"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx new file mode 100644 index 0000000000000..9208808c6ce4c --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx @@ -0,0 +1,30 @@ +// When in doubt, register variables that were used in JSX. +// Foo, Header, and B get registered. +// A doesn't get registered because it's not declared locally. +// Alias doesn't get registered because its definition is just an identifier. + +import A from './A'; +import Store from './Store'; + +Store.subscribe(); + +const Header = styled.div`color: red` +const StyledFactory1 = styled('div')`color: hotpink` +const StyledFactory2 = styled('div')({ color: 'hotpink' }) +const StyledFactory3 = styled(A)({ color: 'hotpink' }) +const FunnyFactory = funny.factory``; + +let Alias1 = A; +let Alias2 = A.Foo; +const Dict = {}; + +function Foo() { + return ( +
+ ); +} + +const B = hoc(A); +// This is currently registered as a false positive: +const NotAComponent = wow(A); +// We could avoid it but it also doesn't hurt. \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/output.js new file mode 100644 index 0000000000000..53c3c42b751a4 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-jsx-at-definition-site/output.js @@ -0,0 +1,32 @@ +import A from "./A"; +import Store from "./Store"; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +Store.subscribe(); +const Header = styled.div`color: red`; +_c = Header; +const StyledFactory1 = styled("div")`color: hotpink`; +_c2 = StyledFactory1; +const StyledFactory2 = styled("div")({ color: "hotpink" }); +_c3 = StyledFactory2; +const StyledFactory3 = styled(A)({ color: "hotpink" }); +_c4 = StyledFactory3; +const FunnyFactory = funny.factory``; +let Alias1 = A; +let Alias2 = A.Foo; +const Dict = {}; +function Foo() { + return _jsxs("div", { children: [_jsx(A, {}), _jsx(B, {}), _jsx(StyledFactory1, {}), _jsx(StyledFactory2, {}), _jsx(StyledFactory3, {}), _jsx(Alias1, {}), _jsx(Alias2, {}), _jsx(Header, {}), _jsx(Dict.X, {})] }); +} +_c5 = Foo; +const B = hoc(A); +_c6 = B; +const NotAComponent = wow(A); +_c7 = NotAComponent; +var _c, _c2, _c3, _c4, _c5, _c6, _c7; +$RefreshReg$(_c, "Header"); +$RefreshReg$(_c2, "StyledFactory1"); +$RefreshReg$(_c3, "StyledFactory2"); +$RefreshReg$(_c4, "StyledFactory3"); +$RefreshReg$(_c5, "Foo"); +$RefreshReg$(_c6, "B"); +$RefreshReg$(_c7, "NotAComponent"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/input.jsx new file mode 100644 index 0000000000000..12b02ebb14cbe --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/input.jsx @@ -0,0 +1,40 @@ +// When in doubt, register variables that were used in JSX. +// Foo, Header, and B get registered. +// A doesn't get registered because it's not declared locally. +// Alias doesn't get registered because its definition is just an identifier. + +import A from './A'; +import Store from './Store'; + +Store.subscribe(); + +const Header = styled.div`color: red` +const StyledFactory1 = styled('div')`color: hotpink` +const StyledFactory2 = styled('div')({ color: 'hotpink' }) +const StyledFactory3 = styled(A)({ color: 'hotpink' }) +const FunnyFactory = funny.factory``; + +let Alias1 = A; +let Alias2 = A.Foo; +const Dict = {}; + +function Foo() { + return [ + React.createElement(A), + React.createElement(B), + React.createElement(StyledFactory1), + React.createElement(StyledFactory2), + React.createElement(StyledFactory3), + React.createElement(Alias1), + React.createElement(Alias2), + jsx(Header), + React.createElement(Dict.X), + ]; +} + +React.createContext(Store); + +const B = hoc(A); +// This is currently registered as a false positive: +const NotAComponent = wow(A); +// We could avoid it but it also doesn't hurt. \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/output.js new file mode 100644 index 0000000000000..f731e51d6bb09 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-identifiers-used-in-react-create-element-at-definition-site/output.js @@ -0,0 +1,32 @@ +import A from "./A"; +import Store from "./Store"; +Store.subscribe(); +const Header = styled.div`color: red`; +_c = Header; +const StyledFactory1 = styled("div")`color: hotpink`; +_c2 = StyledFactory1; +const StyledFactory2 = styled("div")({ color: "hotpink" }); +_c3 = StyledFactory2; +const StyledFactory3 = styled(A)({ color: "hotpink" }); +_c4 = StyledFactory3; +const FunnyFactory = funny.factory``; +let Alias1 = A; +let Alias2 = A.Foo; +const Dict = {}; +function Foo() { + return [React.createElement(A), React.createElement(B), React.createElement(StyledFactory1), React.createElement(StyledFactory2), React.createElement(StyledFactory3), React.createElement(Alias1), React.createElement(Alias2), jsx(Header), React.createElement(Dict.X)]; +} +_c5 = Foo; +React.createContext(Store); +const B = hoc(A); +_c6 = B; +const NotAComponent = wow(A); +_c7 = NotAComponent; +var _c, _c2, _c3, _c4, _c5, _c6, _c7; +$RefreshReg$(_c, "Header"); +$RefreshReg$(_c2, "StyledFactory1"); +$RefreshReg$(_c3, "StyledFactory2"); +$RefreshReg$(_c4, "StyledFactory3"); +$RefreshReg$(_c5, "Foo"); +$RefreshReg$(_c6, "B"); +$RefreshReg$(_c7, "NotAComponent"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/input.jsx new file mode 100644 index 0000000000000..0f68d2db05e13 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/input.jsx @@ -0,0 +1,9 @@ +const A = forwardRef(function() { + return

Foo

; +}); +const B = memo(React.forwardRef(() => { + return

Foo

; +})); +export default React.memo(forwardRef((props, ref) => { + return

Foo

; +})); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/output.js new file mode 100644 index 0000000000000..5f265f0d12dad --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-1/output.js @@ -0,0 +1,21 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +const A = forwardRef(_c = function() { + return _jsx("h1", { children: "Foo" }); +}); +_c2 = A; +const B = memo(_c4 = React.forwardRef(_c3 = () => { + return _jsx("h1", { children: "Foo" }); +})); +_c5 = B; +export default _c8 = React.memo(_c7 = forwardRef(_c6 = (props, ref) => { + return _jsx("h1", { children: "Foo" }); +})); +var _c, _c2, _c3, _c4, _c5, _c6, _c7, _c8; +$RefreshReg$(_c, "A$forwardRef"); +$RefreshReg$(_c2, "A"); +$RefreshReg$(_c3, "B$memo$React.forwardRef"); +$RefreshReg$(_c4, "B$memo"); +$RefreshReg$(_c5, "B"); +$RefreshReg$(_c6, "%default%$React.memo$forwardRef"); +$RefreshReg$(_c7, "%default%$React.memo"); +$RefreshReg$(_c8, "%default%"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/input.jsx new file mode 100644 index 0000000000000..20aa196d47bb6 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/input.jsx @@ -0,0 +1,3 @@ +export default React.memo(forwardRef(function (props, ref) { + return

Foo

; +})); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/output.js new file mode 100644 index 0000000000000..6148fce4645c6 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-2/output.js @@ -0,0 +1,8 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +export default _c3 = React.memo(_c2 = forwardRef(_c = function(props, ref) { + return _jsx("h1", { children: "Foo" }); +})); +var _c, _c2, _c3; +$RefreshReg$(_c, "%default%$React.memo$forwardRef"); +$RefreshReg$(_c2, "%default%$React.memo"); +$RefreshReg$(_c3, "%default%"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/input.jsx new file mode 100644 index 0000000000000..bc646b5b624aa --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/input.jsx @@ -0,0 +1,3 @@ +export default React.memo(forwardRef(function Named(props, ref) { + return

Foo

; +})); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/output.js new file mode 100644 index 0000000000000..e60e3964ff792 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-likely-hocs-with-inline-functions-3/output.js @@ -0,0 +1,8 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +export default _c3 = React.memo(_c2 = forwardRef(_c = function Named(props, ref) { + return _jsx("h1", { children: "Foo" }); +})); +var _c, _c2, _c3; +$RefreshReg$(_c, "%default%$React.memo$forwardRef"); +$RefreshReg$(_c2, "%default%$React.memo"); +$RefreshReg$(_c3, "%default%"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/input.jsx new file mode 100644 index 0000000000000..f76b06ca29de4 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/input.jsx @@ -0,0 +1,18 @@ +export function Hello() { + function handleClick() {} + return

Hi

; +} + +export default function Bar() { + return ; +} + +function Baz() { + return

OK

; +} + +const NotAComp = 'hi'; +export { Baz, NotAComp }; + +export function sum() {} +export const Bad = 42; \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/output.js new file mode 100644 index 0000000000000..f81fa11b93d66 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-function-declarations/output.js @@ -0,0 +1,27 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +export function Hello() { + function handleClick() {} + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); +} +_c = Hello; +export default function Bar() { + return _jsx(Hello, {}); +} +_c2 = Bar; +function Baz() { + return _jsx("h1", { children: "OK" }); +} +_c3 = Baz; +const NotAComp = 'hi'; +export { Baz, NotAComp }; +export function sum() {} +export const Bad = 42; + +var _c, _c2, _c3; + +$RefreshReg$(_c, "Hello"); +$RefreshReg$(_c2, "Bar"); +$RefreshReg$(_c3, "Baz"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/input.jsx new file mode 100644 index 0000000000000..5913c23561eec --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/input.jsx @@ -0,0 +1,12 @@ +export const Hello = () => { + function handleClick() {} + return

Hi

; +}; + +export let Bar = (props) => ; + +export default () => { + // This one should be ignored. + // You should name your components. + return ; +}; \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/output.js new file mode 100644 index 0000000000000..a4a66d9bd9f25 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-exported-named-arrow-functions/output.js @@ -0,0 +1,17 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +export const Hello = () => { + function handleClick() {} + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); +}; +_c = Hello; +export let Bar = (props) => _jsx(Hello, {}); +_c2 = Bar; +export default () => { + return _jsx(Hello, {}); +}; +var _c, _c2; +$RefreshReg$(_c, "Hello"); +$RefreshReg$(_c2, "Bar"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/input.jsx new file mode 100644 index 0000000000000..62e99f5afdce5 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/input.jsx @@ -0,0 +1,8 @@ +function Hello() { + function handleClick() {} + return

Hi

; +} + +function Bar() { + return ; +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/output.js new file mode 100644 index 0000000000000..be4ca90c05950 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-function-declarations/output.js @@ -0,0 +1,23 @@ +import { jsx as _jsx } from "react/jsx-runtime"; + +function Hello() { + function handleClick() {} + + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); +} + +_c = Hello; + +function Bar() { + return _jsx(Hello, {}); +} + +_c2 = Bar; + +var _c, _c2; + +$RefreshReg$(_c, "Hello"); +$RefreshReg$(_c2, "Bar"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/input.jsx new file mode 100644 index 0000000000000..11f8cd8285b03 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/input.jsx @@ -0,0 +1,10 @@ +// Hello, Bar, and Baz should be registered; handleClick and sum shouldn't. +let Hello = () => { + const handleClick = () => {}; + return

Hi

; +} +const Bar = () => { + return ; +}; +var Baz = () =>
; +var sum = () => {}; \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/output.js new file mode 100644 index 0000000000000..87b30e9c6d788 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-arrow-functions/output.js @@ -0,0 +1,20 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +let Hello = () => { + const handleClick = () => {}; + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); +}; +_c = Hello; +const Bar = () => { + return _jsx(Hello, {}); +}; +_c2 = Bar; +var Baz = () => _jsx("div", {}); +_c3 = Baz; +var sum = () => {}; +var _c, _c2, _c3; +$RefreshReg$(_c, "Hello"); +$RefreshReg$(_c2, "Bar"); +$RefreshReg$(_c3, "Baz"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/input.jsx new file mode 100644 index 0000000000000..03ad5077ccca0 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/input.jsx @@ -0,0 +1,12 @@ +// Hello and Bar should be registered; handleClick, sum, Baz, and Qux shouldn't. + +let Hello = function() { + function handleClick() {} + return

Hi

; +}; +const Bar = function Baz() { + return ; +}; +function sum() {} +let Baz = 10; +var Qux; \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/output.js new file mode 100644 index 0000000000000..a4eab96771d5e --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/registers-top-level-variable-declarations-with-function-expressions/output.js @@ -0,0 +1,19 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +let Hello = function() { + function handleClick() {} + return _jsx("h1", { + onClick: handleClick, + children: "Hi" + }); +}; +_c = Hello; +const Bar = function Baz() { + return _jsx(Hello, {}); +}; +_c2 = Bar; +function sum() {} +let Baz = 10; +var Qux; +var _c, _c2; +$RefreshReg$(_c, "Hello"); +$RefreshReg$(_c2, "Bar"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/input.tsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/input.tsx new file mode 100644 index 0000000000000..dcd54f8394dd3 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/input.tsx @@ -0,0 +1,15 @@ +namespace Foo { + export namespace Bar { + export const A = () => {}; + + function B() {}; + export const B1 = B; + } + + export const C = () => {}; + export function D() {}; + + namespace NotExported { + export const E = () => {}; + } +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/output.ts b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/output.ts new file mode 100644 index 0000000000000..9669ed9bc8676 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/supports-typescript-namespace-syntax/output.ts @@ -0,0 +1,25 @@ +namespace Foo { + export namespace Bar { + export const A = () => {}; + _c = A; + function B() {} + _c2 = B; + ; + export const B1 = B; + } + export const C = () => {}; + _c3 = C; + export function D() {} + _c4 = D; + ; + namespace NotExported { + export const E = () => {}; + } +} + +var _c, _c2, _c3, _c4; + +$RefreshReg$(_c, "Foo$Bar$A"); +$RefreshReg$(_c2, "Foo$Bar$B"); +$RefreshReg$(_c3, "Foo$C"); +$RefreshReg$(_c4, "Foo$D"); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/input.jsx new file mode 100644 index 0000000000000..7e871cb8dc04d --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/input.jsx @@ -0,0 +1,4 @@ +export default function Bar () { + useContext(X) + return +}; \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/options.json new file mode 100644 index 0000000000000..be726ca7ef293 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/options.json @@ -0,0 +1,8 @@ +{ + "plugins": [["transform-react-jsx", { + "refresh": { + "refreshReg": "import.meta.refreshReg", + "refreshSig": "import.meta.refreshSig" + } + }]] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/output.js new file mode 100644 index 0000000000000..70bb54098f68b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-custom-identifiers-for-refresh-reg-and-refresh-sig/output.js @@ -0,0 +1,12 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +var _s = import.meta.refreshSig(); +export default function Bar() { + _s(); + useContext(X); + return _jsx(Foo, {}); +} +_s(Bar, "useContext{}"); +_c = Bar; +; +var _c; +import.meta.refreshReg(_c, "Bar"); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/input.jsx new file mode 100644 index 0000000000000..82a86d5fbd52d --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/input.jsx @@ -0,0 +1,4 @@ +function Hello() { + return

Hi

; +} +Hello = connect(Hello); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/output.js new file mode 100644 index 0000000000000..3efb26174be87 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/refresh/uses-original-function-declaration-if-it-get-reassigned/output.js @@ -0,0 +1,8 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +function Hello() { + return _jsx("h1", { children: "Hi" }); +} +_c = Hello; +Hello = connect(Hello); +var _c; +$RefreshReg$(_c, "Hello");