Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transformer): support react fast refresh #4587

Merged

Conversation

Dunqing
Copy link
Member

@Dunqing Dunqing commented Jul 31, 2024

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.

// TODO: Try to remove this struct, avoid double visiting
struct CalculateSignatureKey<'a, 'b> {
key: String,
source_text: &'a str,
ctx: &'b mut TraverseCtx<'a>,
callee_list: Vec<(Atom<'a>, Option<Atom<'a>>)>,
scope_ids: Vec<ScopeId>,
declarator_id_span: Option<Span>,
}
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<oxc_allocator::Vec<'a, Argument<'a>>> {
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::<BindingRestElement>::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::<TSTypeParameterDeclaration>::None,
Option::<TSThisParameter>::None,
formal_parameters,
Option::<TSTypeAnnotation>::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<Option<oxc_semantic::ScopeId>>) {
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 { ")" }
));
}
}

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

@github-actions github-actions bot added the A-transformer Area - Transformer / Transpiler label Jul 31, 2024
Copy link
Member Author

Dunqing commented Jul 31, 2024

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @Dunqing and the rest of your teammates on Graphite Graphite

Copy link

graphite-app bot commented Jul 31, 2024

Your org has enabled the Graphite merge queue for merging into main

Add the label “merge” to the PR and Graphite will automatically add it to the merge queue when it’s ready to merge. Or use the label “hotfix” to add to the merge queue as a hot fix.

You must have a Graphite account and log in to Graphite in order to use the merge queue. Sign up using this link.

Copy link

codspeed-hq bot commented Jul 31, 2024

CodSpeed Performance Report

Merging #4587 will not alter performance

Comparing 07-31-feat_transformer_support_react_fast_refresh (f1fcdde) with main (b3ec9e5)

Summary

✅ 29 untouched benchmarks

@Dunqing Dunqing force-pushed the 07-31-feat_transformer_support_react_fast_refresh branch 4 times, most recently from bb3da37 to d797e59 Compare August 9, 2024 08:53
@Dunqing Dunqing marked this pull request as ready for review August 9, 2024 09:12
@Boshen Boshen self-assigned this Aug 9, 2024
@overlookmotel
Copy link
Collaborator

Is this ready for review? Or blocked on #4746 and #4767?

@Boshen
Copy link
Member

Boshen commented Aug 11, 2024

Is this ready for review? Or blocked on #4746 and #4767?

Ready for review. Can be merged before fixing these.

@Boshen Boshen added the P-high Priority - High label Aug 12, 2024
Copy link
Collaborator

@overlookmotel overlookmotel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I haven't gone fully into this, so I haven't sought to understand all the logic of the plugin, or this implementation. But I get at least a vague sense that maybe there's some duplicated logic in this implementation, especially in transform_statements / transform_statements_on_exit.

I may be wrong about that, but could you please check?

Just to be clear, I don't object to merging this - we could always merge and then iterate. But you've asked me to review, so I'm just giving my thoughts.

Wider point: I think we need a better naming convention to make it easier to recognise which functions are "entry points" from the visitor, and which are functions which are only called by those entry points. I think if we call the entry points transform_* and transform_*_on_exit, we should use different names for the internal methods. When everything is called transform_*, it makes it quite hard to follow the logic.

crates/oxc_transformer/src/react/refresh.rs Outdated Show resolved Hide resolved
crates/oxc_transformer/src/react/refresh.rs Outdated Show resolved Hide resolved
crates/oxc_transformer/src/react/refresh.rs Outdated Show resolved Hide resolved
crates/oxc_transformer/src/react/refresh.rs Outdated Show resolved Hide resolved
crates/oxc_transformer/src/react/refresh.rs Outdated Show resolved Hide resolved
crates/oxc_transformer/src/react/refresh.rs Outdated Show resolved Hide resolved
@Dunqing
Copy link
Member Author

Dunqing commented Aug 13, 2024

Wider point: I think we need a better naming convention to make it easier to recognise which functions are "entry points" from the visitor, and which are functions which are only called by those entry points. I think if we call the entry points transform_* and transform_on_exit, we should use different names for the internal methods. When everything is called transform, it makes it quite hard to follow the logic.

Yes! I agree, I'll try to rename these methods

@Dunqing Dunqing force-pushed the 07-31-feat_transformer_support_react_fast_refresh branch from 8ccf74e to ad0b040 Compare August 13, 2024 14:00
@Dunqing
Copy link
Member Author

Dunqing commented Aug 14, 2024

@overlookmotel Please take a look again, once this is merged, I will start to refactor it according to #4881 and try to resolve the remaining tests

Copy link
Collaborator

@overlookmotel overlookmotel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't review this properly without delving fully into how the transform works.

I agree that we should merge it now. If you're going to refactor it anyway in a follow-on, it'll be easier for me to do a "2nd pass" review after that.

Currently there are merge conflicts. If you're able to resolve those, please go ahead and merge.

@Dunqing Dunqing force-pushed the 07-31-feat_transformer_support_react_fast_refresh branch from 6e3e870 to 1f5016a Compare August 15, 2024 15:00
Copy link

graphite-app bot commented Aug 15, 2024

Merge activity

Dunqing added a commit that referenced this pull request Aug 15, 2024
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..
@Dunqing Dunqing force-pushed the 07-31-feat_transformer_support_react_fast_refresh branch from 1f5016a to 9c2ae8e Compare August 15, 2024 16:39
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..
@Dunqing Dunqing force-pushed the 07-31-feat_transformer_support_react_fast_refresh branch from 9c2ae8e to f1fcdde Compare August 15, 2024 16:41
@graphite-app graphite-app bot merged commit f1fcdde into main Aug 15, 2024
25 checks passed
@graphite-app graphite-app bot deleted the 07-31-feat_transformer_support_react_fast_refresh branch August 15, 2024 16:45
@oxc-bot oxc-bot mentioned this pull request Aug 18, 2024
Boshen added a commit that referenced this pull request Aug 18, 2024
## [0.24.3] - 2024-08-18

### Features

- d49fb16 oxc_codegen: Support generate range leading comments (#4898)
(IWANABETHATGUY)
- 80d0d1f semantic: Check for invalid interface heritage clauses (#4928)
(DonIsaac)
- 48821c0 semantic,syntax: Add SymbolFlags::ArrowFunction (#4946)
(DonIsaac)
- f1fcdde transformer: Support react fast refresh (#4587) (Dunqing)
- 0d79122 transformer: Support logical-assignment-operators plugin
(#4890) (Dunqing)
- ab1d08c transformer: Support `optional-catch-binding` plugin (#4885)
(Dunqing)
- 69da9fd transformer: Support nullish-coalescing-operator plugin
(#4884) (Dunqing)
- 3a66e58 transformer: Support exponentiation operator plugin (#4876)
(Dunqing)
- f88cbcd transformer: Add `BoundIdentifier::new_uid_in_current_scope`
method (#4903) (overlookmotel)
- 1e6d0fe transformer: Add methods to `BoundIdentifier` (#4897)
(overlookmotel)
- fd34640 traverse: Support `generate_uid_based_on_node` method in
`TraverseCtx` (#4940) (Dunqing)
- 72a37fc traverse: Support `clone_identifier_reference` method in
`TraverseCtx` (#4880) (Dunqing)

### Bug Fixes

- c0b26f4 ast: Do not include `scope_id` fields in JSON AST (#4858)
(overlookmotel)
- bbf9ec0 codegen: Add missing `declare` to `PropertyDefinition` (#4937)
(Boshen)
- f210cf7 codegen: Print `TSSatisfiesExpression` and
`TSInstantiationExpression` (#4936) (Boshen)
- 21f5762 codegen: Minify large numbers (#4889) (Boshen)
- e8de4bd codegen: Fix whitespace issue when minifying `x in new
Error()` (#4886) (Boshen)
- a226962 codegen: Print `TSNonNullExpression` (#4869) (Boshen)
- 3da33d3 codegen: Missing parenthesis for `PrivateInExpression` (#4865)
(Boshen)
- 1808529 codegen: Dedupe pure annotation comments (#4862)
(IWANABETHATGUY)
- d3bbc62 isolated-declarations: Declare modifier of PropertyDefinition
should not be retained (#4941) (Dunqing)
- 8e80f59 isolated_declarations: Class properties should still be lifted
from private constructors (#4934) (michaelm)
- b3ec9e5 isolated_declarations: Always emit module declarations that
perform augmentation (#4919) (michaelm)
- 0fb0b71 isolated_declarations: Always emit module declarations (#4911)
(michaelm)
- 4a16916 isolated_declarations: Support expando functions (#4910)
(michaelm)
- 508644a linter/tree-shaking: Correct the calculation of `>>`, `<<` and
`>>>` (#4932) (mysteryven)
- 46cb1c1 minifier: Handle `Object.definedPropert(exports` for
@babel/types/lib/index.js (#4933) (Boshen)
- 81fd637 minifier: Do not fold `0 && (module.exports = {})` for
`cjs-module-lexer` (#4878) (Boshen)
- 879a271 minifier: Do not join `require` calls for `cjs-module-lexer`
(#4875) (Boshen)
- 1bdde2c parser: Detect @flow in `/** @flow */ comment (#4861) (Boshen)
- 2476dce transformer: Remove an `ast.copy` from
`NullishCoalescingOperator` transform (#4913) (overlookmotel)
- 248a757 transformer/typescript: Typescript syntax within
`SimpleAssignmentTarget` with `MemberExpressions` is not stripped
(#4920) (Dunqing)

### Documentation

- 47c9552 ast, ast_macros, ast_tools: Better documentation for `Ast`
helper attributes. (#4856) (rzvxa)
- 0a01a47 semantic: Improve documentation (#4850) (DonIsaac)
- 9c700ed transformer: Add README including style guide (#4899)
(overlookmotel)

### Refactor

- a6967b3 allocator: Correct code comment (#4904) (overlookmotel)
- 90d0b2b allocator, ast, span, ast_tools: Use `allocator` as var name
for `Allocator` (#4900) (overlookmotel)
- 1eb59d2 ast, isolated_declarations, transformer: Mark
`AstBuilder::copy` as an unsafe function (#4907) (overlookmotel)
- 8e8fcd0 ast_tools: Rename `oxc_ast_codegen` to `oxc_ast_tools`.
(#4846) (rzvxa)
- 786bf07 index: Shorten code and correct comment (#4905)
(overlookmotel)
- ea1e64a semantic: Make SemanticBuilder opaque (#4851) (DonIsaac)
- 5fd1701 sourcemap: Lower the `msrv`. (#4873) (rzvxa)
- 48a1c32 syntax: Inline trivial bitflags methods (#4877)
(overlookmotel)
- 452187a transformer: Rename `BoundIdentifier::new_uid_in_root_scope`
(#4902) (overlookmotel)
- 707a01f transformer: Re-order `BoundIdentifier` methods (#4896)
(overlookmotel)
- 117dff2 transformer: Improve comments for `BoundIdentifier` helper
(#4895) (overlookmotel)

Co-authored-by: Boshen <[email protected]>
overlookmotel pushed a commit that referenced this pull request Aug 30, 2024
…ation from refresh (#5289)

follow-up: #4587 (comment)

The `CalculateSignatureKey`is used to collect signature keys, but since it requires a double visit, it doesn't perform very well. Now I use ScopeId to store the signature key that is generated in `CallExpression`. This way we can then determine which ArrowFunction/Function the `CallExpression` belongs to.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-transformer Area - Transformer / Transpiler P-high Priority - High
Projects
None yet
Development

Successfully merging this pull request may close these issues.

transformer: React fast refresh transform
3 participants