Add support for JSX fragment syntax#19249
Conversation
src/compiler/checker.ts
Outdated
| // which is a type of the parameter of the signature we are trying out. | ||
| // If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName | ||
| const attributesType = getContextualType(jsxAttributes); | ||
| if (jsxAttributes) { |
There was a problem hiding this comment.
Just do an early return of undefined here if jsxAttributes is undefined
|
|
||
| function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) { | ||
| checkGrammarJsxElement(node); | ||
| function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) { |
There was a problem hiding this comment.
No need to rename this - a fragment is an element (IMHO?)
src/compiler/parser.ts
Outdated
| else if (opening.kind === SyntaxKind.JsxOpeningFragment) { | ||
| const node = <JsxFragment>createNode(SyntaxKind.JsxFragment, opening.pos); | ||
| node.openingFragment = opening; | ||
|
|
src/compiler/parser.ts
Outdated
| // If we hit EOF, issue the error at the tag that lacks the closing element | ||
| // rather than at the end of the file (which is useless) | ||
| parseErrorAtPosition(openingTagName.pos, openingTagName.end - openingTagName.pos, Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, getTextOfNodeFromSourceText(sourceText, openingTagName)); | ||
| if (isJsxOpeningElement(openingTag)) { |
There was a problem hiding this comment.
Reading this whole thing it seems like it'd be clearer if we wrote e.g. if (isJsxFragment(openingTag)) instead of isJsxOpeningElement since the former is more the thing we're special-casing
There was a problem hiding this comment.
Restructured.
| return finishNode(node); | ||
| } | ||
|
|
||
| function parseJsxClosingFragment(inExpressionContext: boolean): JsxClosingFragment { |
There was a problem hiding this comment.
Reading the spec, I don't see how we'd legally get into the inExpressionContext = false case since you can't write <foo><><div /></></foo>
There was a problem hiding this comment.
The spec changed, now you can.
There was a problem hiding this comment.
src/compiler/scanner.ts
Outdated
|
|
||
| /* @internal */ | ||
| export function tokenIsIdentifierOrKeywordOrGreaterThan(token: SyntaxKind): boolean { | ||
| return token === SyntaxKind.GreaterThanToken || token >= SyntaxKind.Identifier; |
There was a problem hiding this comment.
Call tokenIsIdentifierOrKeyword in the right arm in case that logic changes
src/compiler/types.ts
Outdated
| } | ||
|
|
||
| /// Either the opening tag in a <Tag>...</Tag> pair, or the lone <Tag /> in a self-closing form | ||
| /// Either the opening tag in a <Tag>...</Tag> pair, the opening tag in a <>...</> pair, or the lone <Tag /> in a self-closing form |
There was a problem hiding this comment.
This comment doesn't match our usage/terminology of isJsxOpeningLikeElement
| } | ||
|
|
||
| // OK | ||
| let k1 = <Comp a={10} b="hi"><></><Button /><AnotherButton /></Comp>; |
There was a problem hiding this comment.
This should be a parse error
There was a problem hiding this comment.
Or perhaps we should parse it leniently but issue a grammar error in checking
|
@mhegazy could you take a look at this? |
|
Editor actions work as expected in VS and VS Code (i.e., go to def, find all references, etc are basically no-ops or return no results). I will be updating the syntax highlighter accordingly. |
src/compiler/checker.ts
Outdated
| // If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName | ||
| const attributesType = getContextualType(jsxAttributes); | ||
| if (!jsxAttributes) { | ||
| return anyType; // don't check children of a fragment |
There was a problem hiding this comment.
It's better to return undefined than anyType here because it will cause the calling code to bail out earlier
|
Tracking syntax highlighter changes for VS Code at microsoft/TypeScript-TmLanguage#534. |
| const tagName = createPropertyAccess( | ||
| createReactNamespace(reactNamespace, parentElement), | ||
| "Fragment" | ||
| ); |
There was a problem hiding this comment.
Should this perhaps be customizable, similar to how --jsxFactory is? e.g. --jsxFragmentComponent?
Arguably not useful for anything but React and React clones, though; as other less similar frameworks would want to do other special processing on fragments.
There was a problem hiding this comment.
I agree with your second point here; to take the example of Mithril, the framework supports fragments, but with a completely different approach than React will (i.e. m.fragment(attrs, children)) so it's not as convenient as simply replacing the call to createElement with --jsxFactory. It seems to make sense in this case to just preserve the JSX syntax as is for non-React libs and have each define their own transpilation depending on their implementation of fragments.
There was a problem hiding this comment.
@Kovensky do you know what babel will do here? would it support another version of the paragma for the React.createElement?
There was a problem hiding this comment.
checked with @hzoo over slack and does not seem that babel have plans for this in the immediate term.
I wounder if we should just make it an error to use Fragment + --jsx React + --jsxNamespace
There was a problem hiding this comment.
@uniqueiniquity can you file an issue for adding the error.
There was a problem hiding this comment.
Oh, I think I see now. I'm confusing jsxFactory and reactNamespace
There was a problem hiding this comment.
So are you on board with issuing the error?
There was a problem hiding this comment.
Probably, but feel free not to block this PR on it.
There was a problem hiding this comment.
Updated the PR to issue an error.
There was a problem hiding this comment.
In the Babel PR, I add a pragma for fragments, as @mhegazy suggested. https://github.com/babel/babel/pull/6552/files#diff-7e5b807d66ce513f501563ad8f8d03e6R117
https://github.com/babel/babel/pull/6552/files#diff-7e5b807d66ce513f501563ad8f8d03e6R89
| } | ||
|
|
||
| // OK | ||
| let k4 = <SingleChildComp a={10} b="hi"><><Button /><AnotherButton /></></SingleChildComp>; |
There was a problem hiding this comment.
I'd kinda prefer for these to be a different type like, say, JSX.Fragment, but having one would not be useful anyway without nominal typing.
There was a problem hiding this comment.
We'd like to make JSX.Element generic in the future - possibly looking it up and instantiating it based on the return type of the factory function; that would handle the Fragment case somewhat elegantly.
There was a problem hiding this comment.
Generic JSX.Element<T> would be great. Outside of the React bubble, TypeScript-oriented developers like myself often wonder why the disconnect.
src/compiler/diagnosticMessages.json
Outdated
| "category": "Error", | ||
| "code": 17014 | ||
| }, | ||
| "Expected corresponding JSX fragment closing tag.": { |
There was a problem hiding this comment.
Expected corresponding closing tag for JSX fragment.
src/compiler/emitter.ts
Outdated
|
|
||
| if (isJsxOpeningElement(node)) { | ||
| emitJsxTagName(node.tagName); | ||
| writeIfAny(node.attributes.properties, " "); |
There was a problem hiding this comment.
You should move this into the below if check and replace the writeIfAny with a write
|
Looks reasonable apart from the nits I requested changes on. 👍 |
src/compiler/checker.ts
Outdated
| function getContextualTypeForJsxExpression(node: JsxExpression): Type { | ||
| // JSX expression can appear in two position : JSX Element's children or JSX attribute | ||
| const jsxAttributes = isJsxAttributeLike(node.parent) ? | ||
| const jsxAttributes: JsxAttributes = isJsxAttributeLike(node.parent) ? |
There was a problem hiding this comment.
Why do you need this type annotation?
Fixes #19094, which in turn reflects facebook/jsx#93.
This PR adds support for the
<>...</>syntax for fragments in JSX. Underpreserveemit, the syntax is maintained, and underreactemit, fragment blocks are transformed intoReact.createElement(React.Fragment, null, ...)(and are thus affected by--jsxFactory). Due to similarities with theJSXElementsyntax, the implementation shares much of the same code where appropriate.