-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(es/typescript): preserve Flow component type semantics #12090
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
base: main
Are you sure you want to change the base?
Changes from 11 commits
c412458
ea9faf1
a1ce40a
0a36bef
29e5518
6f4ff0c
96c8e8b
be0d456
96e4f18
0889408
37e28bd
0cc3704
7f2ce67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| swc: patch | ||
| swc_ecma_codegen: patch | ||
| swc_ecma_transforms_typescript: patch | ||
| --- | ||
|
|
||
| fix(es/typescript): Preserve Flow component type semantics |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -317,6 +317,71 @@ impl MacroNode for TsFnType { | |
| fn emit(&mut self, emitter: &mut Macro) -> Result { | ||
| emitter.emit_leading_comments_of_span(self.span(), false)?; | ||
|
|
||
| if is_flow_component_type(self) { | ||
| keyword!(emitter, "component"); | ||
| emit!(self.type_params); | ||
|
|
||
| punct!(emitter, "("); | ||
| let [TsFnParam::Object(props)] = self.params.as_slice() else { | ||
| unreachable!("Flow component types have one object-pattern parameter") | ||
| }; | ||
| for (index, prop) in props.props.iter().enumerate() { | ||
| if index != 0 { | ||
| punct!(emitter, ","); | ||
| formatting_space!(emitter); | ||
| } | ||
|
|
||
| match prop { | ||
| ObjectPatProp::KeyValue(prop) => { | ||
| let is_shorthand = match prop.value.as_ref() { | ||
| Pat::Ident(binding) => match &prop.key { | ||
| PropName::Ident(key) => key.sym == binding.id.sym, | ||
| _ => false, | ||
| }, | ||
| Pat::Assign(assign) => match assign.left.as_ref() { | ||
| Pat::Ident(binding) => match &prop.key { | ||
| PropName::Ident(key) => key.sym == binding.id.sym, | ||
| _ => false, | ||
| }, | ||
| _ => false, | ||
| }, | ||
| _ => false, | ||
| }; | ||
|
|
||
| if is_shorthand { | ||
| emit!(prop.value); | ||
| } else { | ||
| emit!(prop.key); | ||
| formatting_space!(emitter); | ||
| keyword!(emitter, "as"); | ||
| formatting_space!(emitter); | ||
|
Comment on lines
+379
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When minifying an aliased component prop such as Useful? React with 👍 / 👎. |
||
| emit!(prop.value); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For Flow component props that are both optional and aliased, e.g. Useful? React with 👍 / 👎.
Comment on lines
+377
to
+382
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a component type with a string-literal prop written without an alias, e.g. Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
| ObjectPatProp::Assign(prop) => emit!(prop), | ||
| ObjectPatProp::Rest(prop) => emit!(prop), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a component type uses Flow's spread-prop rest syntax like Useful? React with 👍 / 👎. |
||
| #[cfg(swc_ast_unknown)] | ||
| _ => return Err(unknown_error()), | ||
| } | ||
| } | ||
| punct!(emitter, ")"); | ||
|
|
||
| if !matches!( | ||
| self.type_ann.type_ann.as_ref(), | ||
| TsType::TsKeywordType(TsKeywordType { | ||
| kind: TsKeywordTypeKind::TsAnyKeyword, | ||
| .. | ||
| }) | ||
| ) { | ||
| formatting_space!(emitter); | ||
| keyword!(emitter, "renders"); | ||
| space!(emitter); | ||
| emit!(self.type_ann); | ||
| } | ||
|
|
||
| return Ok(()); | ||
| } | ||
|
|
||
| emit!(self.type_params); | ||
|
|
||
| punct!(emitter, "("); | ||
|
|
@@ -332,6 +397,23 @@ impl MacroNode for TsFnType { | |
| } | ||
| } | ||
|
|
||
| /// Returns whether this function type is the parser's representation of a | ||
| /// Flow `component(...)` type. | ||
| /// | ||
| /// Flow component parameters describe a single props object. The parser | ||
| /// preserves that syntax without extending the public AST by giving the | ||
| /// synthetic object pattern the same non-dummy span as the function type. | ||
| #[inline] | ||
| fn is_flow_component_type(fn_type: &TsFnType) -> bool { | ||
| matches!( | ||
| fn_type.params.as_slice(), | ||
| [TsFnParam::Object(props)] | ||
| if !fn_type.span.is_dummy() | ||
| && props.span == fn_type.span | ||
| && props.type_ann.is_none() | ||
| ) | ||
| } | ||
|
|
||
| #[node_impl] | ||
| impl MacroNode for TsImportEqualsDecl { | ||
| fn emit(&mut self, emitter: &mut Macro) -> Result { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "flow": true, | ||
| "flow_components": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| type ComponentType<T> = component(ref?: T, ...props: mixed) renders React.Node; | ||
| type FunctionType<T> = (props: T) => React.Node; | ||
| type DestructuredFunctionType = ({value}: mixed) => mixed; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| type ComponentType<T> = component(ref?: T, ...props: mixed) renders React.Node; | ||
| type FunctionType<T> = (props: T) => React.Node; | ||
| type DestructuredFunctionType = ({ value }: mixed) => mixed; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| type ComponentType<T>=component(ref?:T,...props:mixed)renders React.Node;type FunctionType<T>=(props:T)=>React.Node;type DestructuredFunctionType=({value}: mixed)=>mixed; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| type ComponentType = component(ref?: mixed, ...props: mixed); | ||
| type FunctionType = (props: mixed) => mixed; | ||
| type HookType = hook (mixed) => mixed; | ||
|
|
||
| const MyComponent: component(ref?: mixed, ...props: mixed) = ({ | ||
| ref, | ||
| ...rest | ||
| }) => null; | ||
| export const ExportedComponent: component(value: mixed) = value => value; | ||
| const OrdinaryArrow: (value: mixed) => mixed = value => value; | ||
| const HookArrow: hook (mixed) => mixed = value => value; | ||
| const UntypedArrow = value => value; | ||
| const ExistingFunction: component() = function() { | ||
| return null; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds the React Native mock regression as an inline
#[test], but the repository asks new coverage to prefer fixture suites; this case can live with the existing Flow strip fixtures instead of embedding source and assertions directly in the harness, and the same pattern also appears in the transform crate test added by this change.AGENTS.md reference: AGENTS.md:L47-L47
Useful? React with 👍 / 👎.