Skip to content

Commit

Permalink
Implement Fragment handling
Browse files Browse the repository at this point in the history
  • Loading branch information
sirreal committed Apr 30, 2019
1 parent c2c8276 commit 6f5fa88
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 42 deletions.
90 changes: 63 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 All @@ -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' );
state._options.scopeVariableFrag = null;
}
}

return state._options;
}

return {
visitor: {
JSXElement( path, state ) {
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 );
}
},
},
},
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( 'adds 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, Fragment } 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;
}

0 comments on commit 6f5fa88

Please sign in to comment.