-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Babel plugin JSX: Implement Fragment handling #15120
Changes from 1 commit
6f5fa88
13a2d6f
635798d
1ec0ebf
c6ae6b4
36faddf
9e13d18
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 |
---|---|---|
@@ -1,17 +1,20 @@ | ||
/** | ||
* Default options for the plugin. | ||
* | ||
* @property {string} scopeVariable Name of variable required to be in scope | ||
* for use by the JSX pragma. For the default | ||
* pragma of React.createElement, the React | ||
* variable must be within scope. | ||
* @property {string} source The module from which the scope variable | ||
* is to be imported when missing. | ||
* @property {boolean} isDefault Whether the scopeVariable is the default | ||
* import of the source module. | ||
* @property {string} scopeVariable Name of variable required to be in scope | ||
* for use by the JSX pragma. For the default | ||
* pragma of React.createElement, the React | ||
* variable must be within scope. | ||
* @property {string} scopeVariableFrag Name of variable required to be in scope | ||
* for use by the Fragment pragma. | ||
* @property {string} source The module from which the scope variable | ||
* is to be imported when missing. | ||
* @property {boolean} isDefault Whether the scopeVariable is the default | ||
* import of the source module. | ||
*/ | ||
const DEFAULT_OPTIONS = { | ||
scopeVariable: 'React', | ||
scopeVariableFrag: null, | ||
source: 'react', | ||
isDefault: true, | ||
}; | ||
|
@@ -32,47 +35,80 @@ module.exports = function( babel ) { | |
function getOptions( state ) { | ||
if ( ! state._options ) { | ||
state._options = Object.assign( {}, DEFAULT_OPTIONS, state.opts ); | ||
if ( state._options.isDefault && state._options.scopeVariableFrag ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( 'scopeVariableFrag is only available when isDefault is false' ); | ||
sirreal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
state._options.scopeVariableFrag = null; | ||
} | ||
} | ||
|
||
return state._options; | ||
} | ||
|
||
return { | ||
visitor: { | ||
JSXElement( path, state ) { | ||
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. Can you explain why this was changed? 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. Yes, I noticed that without this change, the following test fails (added in this PR): it( 'adds import for scope variable for Fragments', () => {
const original = 'let foo = <></>;';
const string = getTransformedCode( original );
expect( string ).toBe( 'import React from "react";\nlet foo = <></>;' );
} );
It seems that It seems that |
||
JSX( path, state ) { | ||
if ( state.hasUndeclaredScopeVariable ) { | ||
return; | ||
} | ||
|
||
const { scopeVariable } = getOptions( state ); | ||
state.hasUndeclaredScopeVariable = ! path.scope.hasBinding( scopeVariable ); | ||
}, | ||
'JSXElement|JSXFragment'( path, state ) { | ||
if ( state.hasUndeclaredScopeVariableFrag ) { | ||
return; | ||
} | ||
|
||
const { scopeVariableFrag } = getOptions( state ); | ||
if ( scopeVariableFrag === null ) { | ||
return; | ||
} | ||
|
||
if ( | ||
path.type === 'JSXFragment' || | ||
( path.type === 'JSXElement' && path.node.openingElement.name.name === 'Fragment' ) | ||
) { | ||
state.hasUndeclaredScopeVariableFrag = ! path.scope.hasBinding( scopeVariableFrag ); | ||
} | ||
}, | ||
Program: { | ||
exit( path, state ) { | ||
if ( ! state.hasUndeclaredScopeVariable ) { | ||
return; | ||
} | ||
const { scopeVariable, scopeVariableFrag, source, isDefault } = getOptions( state ); | ||
|
||
const { scopeVariable, source, isDefault } = getOptions( state ); | ||
let scopeVariableSpecifier; | ||
let scopeVariableFragSpecifier; | ||
|
||
let specifier; | ||
if ( isDefault ) { | ||
specifier = t.importDefaultSpecifier( | ||
t.identifier( scopeVariable ) | ||
); | ||
} else { | ||
specifier = t.importSpecifier( | ||
t.identifier( scopeVariable ), | ||
t.identifier( scopeVariable ) | ||
if ( state.hasUndeclaredScopeVariable ) { | ||
if ( isDefault ) { | ||
scopeVariableSpecifier = t.importDefaultSpecifier( t.identifier( scopeVariable ) ); | ||
} else { | ||
scopeVariableSpecifier = t.importSpecifier( | ||
t.identifier( scopeVariable ), | ||
t.identifier( scopeVariable ) | ||
); | ||
} | ||
} | ||
|
||
if ( state.hasUndeclaredScopeVariableFrag ) { | ||
scopeVariableFragSpecifier = t.importSpecifier( | ||
t.identifier( scopeVariableFrag ), | ||
t.identifier( scopeVariableFrag ) | ||
); | ||
} | ||
|
||
const importDeclaration = t.importDeclaration( | ||
[ specifier ], | ||
t.stringLiteral( source ) | ||
); | ||
const importDeclarationSpecifiers = [ | ||
scopeVariableSpecifier, | ||
scopeVariableFragSpecifier, | ||
].filter( Boolean ); | ||
if ( importDeclarationSpecifiers.length ) { | ||
const importDeclaration = t.importDeclaration( | ||
importDeclarationSpecifiers, | ||
t.stringLiteral( source ) | ||
); | ||
|
||
path.unshiftContainer( 'body', importDeclaration ); | ||
path.unshiftContainer( 'body', importDeclaration ); | ||
} | ||
}, | ||
}, | ||
}, | ||
|
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.
So a consumer must opt in to this? Can there not be a default, like there is with
@babel/plugin-transform-react-jsx
?Is the expected behavior when this is
null
to not do anything when it encounters a fragment?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.
I think
null
matches the default expectations across the board. When it'snull
and<></>
Fragments are encountered, no named import is added. However, this is JSX so thescopeVariable
will be added.If we consider the defaults (they could be omitted here):
Note that
pragmaFrag
's default isReact.Fragment
. With default arguments,<>In a Fragment</>
will result in something like: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.
Oh! That's quite tricky to follow, but it makes sense. Thanks for explaining.