Skip to content

Commit

Permalink
Babel plugin JSX: Implement Fragment handling (#15120)
Browse files Browse the repository at this point in the history
Add imports for `<></>` JSX Fragments.
  • Loading branch information
sirreal authored Apr 30, 2019
1 parent 96cad99 commit d715e93
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 44 deletions.
8 changes: 7 additions & 1 deletion packages/babel-plugin-import-jsx-pragma/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.1.0 (unreleased)

### New Feature

- Add Fragment import handling ([#15120](https://github.com/WordPress/gutenberg/pull/15120)).

## 2.0.0 (2019-03-06)

### Breaking Change
Expand All @@ -6,7 +12,7 @@

### Enhancement

- Plugin skips now adding import JSX pragma when the scope variable is defined for all JSX elements ([#13809](https://github.com/WordPress/gutenberg/pull/13809)).
- Plugin skips now adding import JSX pragma when the scope variable is defined for all JSX elements ([#13809](https://github.com/WordPress/gutenberg/pull/13809)).

## 1.1.0 (2018-09-05)

Expand Down
12 changes: 11 additions & 1 deletion packages/babel-plugin-import-jsx-pragma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ module.exports = {
plugins: [
[ '@wordpress/babel-plugin-import-jsx-pragma', {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} ],
[ '@babel/transform-react-jsx', {
pragma: 'createElement',
pragmaFrag: 'Fragment',
} ],
],
};
Expand All @@ -60,6 +62,13 @@ _Type:_ String

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.

### `scopeVariableFrag`

_Type:_ String

Name of variable required to be in scope for `<></>` `Fragment` JSX. Named `<Fragment />` elements
expect Fragment to be in scope and will not add the import.

### `source`

_Type:_ String
Expand All @@ -70,6 +79,7 @@ The module from which the scope variable is to be imported when missing.

_Type:_ Boolean

Whether the scopeVariable is the default import of the source module.
Whether the scopeVariable is the default import of the source module. Note that this has no impact
on `scopeVariableFrag`.

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
80 changes: 53 additions & 27 deletions packages/babel-plugin-import-jsx-pragma/index.js
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,
};
Expand Down Expand Up @@ -39,40 +42,63 @@ module.exports = function( babel ) {

return {
visitor: {
JSXElement( path, state ) {
JSX( path, state ) {
if ( state.hasUndeclaredScopeVariable ) {
return;
}

const { scopeVariable } = getOptions( state );
state.hasUndeclaredScopeVariable = ! path.scope.hasBinding( scopeVariable );
},
JSXFragment( path, state ) {
if ( state.hasUndeclaredScopeVariableFrag ) {
return;
}

const { scopeVariableFrag } = getOptions( state );
if ( scopeVariableFrag === null ) {
return;
}

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 );
}
},
},
},
Expand Down
99 changes: 84 additions & 15 deletions packages/babel-plugin-import-jsx-pragma/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,6 @@ import { transformSync } from '@babel/core';
import plugin from '../';

describe( 'babel-plugin-import-jsx-pragma', () => {
function getTransformedCode( source, options = {} ) {
const { code } = transformSync( source, {
configFile: false,
plugins: [
[ plugin, options ],
'@babel/plugin-syntax-jsx',
],
} );

return code;
}

it( 'does nothing if there is no jsx', () => {
const original = 'let foo;';
const string = getTransformedCode( original );
Expand Down Expand Up @@ -49,6 +37,13 @@ describe( 'babel-plugin-import-jsx-pragma', () => {
expect( string ).toBe( 'import React from "react";\n' + original );
} );

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( 'allows options customization', () => {
const original = 'let foo = <bar />;';
const string = getTransformedCode( original, {
Expand All @@ -61,7 +56,8 @@ describe( 'babel-plugin-import-jsx-pragma', () => {
} );

it( 'adds import for scope variable even when defined inside the local scope', () => {
const original = 'let foo = <bar />;\n\nfunction local() {\n const createElement = wp.element.createElement;\n}';
const original =
'let foo = <bar />;\n\nfunction local() {\n const createElement = wp.element.createElement;\n}';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
source: '@wordpress/element',
Expand All @@ -72,20 +68,93 @@ describe( 'babel-plugin-import-jsx-pragma', () => {
} );

it( 'does nothing if the outer scope variable is already defined when using custom options', () => {
const original = 'const {\n createElement\n} = wp.element;\nlet foo = <bar />;';
const original =
'const {\n createElement,\n Fragment\n} = wp.element;\nlet foo = <><bar /></>;';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} );

expect( string ).toBe( original );
} );

it( 'adds only Fragment when required', () => {
const original = 'const {\n createElement\n} = wp.element;\nlet foo = <><bar /></>;';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} );

expect( string ).toBe(
'import { Fragment } from "@wordpress/element";\nconst {\n createElement\n} = wp.element;\nlet foo = <><bar /></>;'
);
} );

it( 'adds only createElement when required', () => {
const original = 'const {\n Fragment\n} = wp.element;\nlet foo = <><bar /></>;';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} );

expect( string ).toBe(
'import { createElement } from "@wordpress/element";\nconst {\n Fragment\n} = wp.element;\nlet foo = <><bar /></>;'
);
} );

it( 'does nothing if the inner scope variable is already defined when using custom options', () => {
const original = '(function () {\n const {\n createElement\n } = wp.element;\n let foo = <bar />;\n})();';
const original =
'(function () {\n const {\n createElement\n } = wp.element;\n let foo = <bar />;\n})();';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} );

expect( string ).toBe( original );
} );

it( 'adds Fragment as for <></>', () => {
const original = 'let foo = <><bar /><baz /></>;';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} );

expect( string ).toBe(
'import { createElement, Fragment } from "@wordpress/element";\nlet foo = <><bar /><baz /></>;'
);
} );

it( 'does not add Fragment import for <Fragment />', () => {
const original = 'let foo = <Fragment><bar /><baz /></Fragment>;';
const string = getTransformedCode( original, {
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
} );

expect( string ).toBe(
'import { createElement } from "@wordpress/element";\nlet foo = <Fragment><bar /><baz /></Fragment>;'
);
} );
} );

function getTransformedCode( source, options = {} ) {
const { code } = transformSync( source, {
configFile: false,
plugins: [ [ plugin, options ], '@babel/plugin-syntax-jsx' ],
} );

return code;
}
6 changes: 6 additions & 0 deletions packages/babel-preset-default/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 4.1.0 (unreleased)

### New Feature

- Handle `<></>` JSX Fragments with `@wordpress/element` `Fragment` ([#15120](https://github.com/WordPress/gutenberg/pull/15120)).

## 4.0.0 (2019-03-06)

### Breaking Change
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-preset-default/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ module.exports = function( api ) {
require.resolve( '@wordpress/babel-plugin-import-jsx-pragma' ),
{
scopeVariable: 'createElement',
scopeVariableFrag: 'Fragment',
source: '@wordpress/element',
isDefault: false,
},
],
[ require.resolve( '@babel/plugin-transform-react-jsx' ), {
pragma: 'createElement',
pragmaFrag: 'Fragment',
} ],
require.resolve( '@babel/plugin-proposal-async-generator-functions' ),
maybeGetPluginTransformRuntime(),
Expand Down

0 comments on commit d715e93

Please sign in to comment.