Skip to content

Commit

Permalink
Framework: Add package: @wordpress/babel-plugin-import-jsx-pragma (#7493
Browse files Browse the repository at this point in the history
)

* Framework: Add package: @wordpress/babel-plugin-import-jsx-pragma

* Build: Prebuild babel-plugin-import-jsx-pragma package

* Build: Improve import-jsx-pragma docs per review feedback

* Build: Consolidate directory testing into filterPackages

* Build Tools: Leverage plugin state in place of scope variables
  • Loading branch information
aduth authored Jun 27, 2018
1 parent e197ba0 commit 6a52599
Show file tree
Hide file tree
Showing 15 changed files with 358 additions and 25 deletions.
8 changes: 8 additions & 0 deletions bin/packages/get-babel-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ const plugins = map( babelDefaultConfig.plugins, ( plugin ) => {
return plugin;
} );

if ( process.env.TRANSFORM_JSX_PRAGMA ) {
plugins.push( [ require( '../../packages/babel-plugin-import-jsx-pragma' ).default, {
scopeVariable: 'createElement',
source: '@wordpress/element',
isDefault: false,
} ] );
}

const babelConfigs = {
main: Object.assign(
{},
Expand Down
65 changes: 62 additions & 3 deletions bin/packages/get-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,71 @@
*/
const fs = require( 'fs' );
const path = require( 'path' );
const { overEvery, compact, includes, negate } = require( 'lodash' );

/**
* Module Constants
* Absolute path to packages directory.
*
* @type {string}
*/
const PACKAGES_DIR = path.resolve( __dirname, '../../packages' );

const {
/**
* Comma-separated string of packages to include in build.
*
* @type {string}
*/
INCLUDE_PACKAGES,

/**
* Comma-separated string of packages to exclude from build.
*
* @type {string}
*/
EXCLUDE_PACKAGES,
} = process.env;

/**
* Given a comma-separated string, returns a filter function which returns true
* if the item is contained within as a comma-separated entry.
*
* @param {Function} filterFn Filter function to call with item to test.
* @param {string} list Comma-separated list of items.
*
* @return {Function} Filter function.
*/
const createCommaSeparatedFilter = ( filterFn, list ) => {
const listItems = list.split( ',' );
return ( item ) => filterFn( listItems, item );
};

/**
* Returns true if the given base file name for a file within the packages
* directory is itself a directory.
*
* @param {string} file Packages directory file.
*
* @return {boolean} Whether file is a directory.
*/
function isDirectory( file ) {
return fs.lstatSync( path.resolve( PACKAGES_DIR, file ) ).isDirectory();
}

/**
* Filter predicate, returning true if the given base file name is to be
* included in the build.
*
* @param {string} pkg File base name to test.
*
* @return {boolean} Whether to include file in build.
*/
const filterPackages = overEvery( compact( [
isDirectory,
INCLUDE_PACKAGES && createCommaSeparatedFilter( includes, INCLUDE_PACKAGES ),
EXCLUDE_PACKAGES && createCommaSeparatedFilter( negate( includes ), EXCLUDE_PACKAGES ),
] ) );

/**
* Returns the absolute path of all WordPress packages
*
Expand All @@ -17,8 +76,8 @@ const PACKAGES_DIR = path.resolve( __dirname, '../../packages' );
function getPackages() {
return fs
.readdirSync( PACKAGES_DIR )
.map( ( file ) => path.resolve( PACKAGES_DIR, file ) )
.filter( ( f ) => fs.lstatSync( path.resolve( f ) ).isDirectory() );
.filter( filterPackages )
.map( ( file ) => path.resolve( PACKAGES_DIR, file ) );
}

module.exports = getPackages;
16 changes: 1 addition & 15 deletions eslint/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ module.exports = {
'react',
'jsx-a11y',
],
settings: {
react: {
pragma: 'wp',
},
},
rules: {
'array-bracket-spacing': [ 'error', 'always' ],
'arrow-parens': [ 'error', 'always' ],
Expand Down Expand Up @@ -117,6 +112,7 @@ module.exports = {
'react/jsx-tag-spacing': 'error',
'react/no-children-prop': 'off',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
semi: 'error',
'semi-spacing': 'error',
'space-before-blocks': [ 'error', 'always' ],
Expand Down Expand Up @@ -160,14 +156,4 @@ module.exports = {
'valid-typeof': 'error',
yoda: 'off',
},
overrides: [
{
files: 'packages/**/*.js',
settings: {
react: {
pragma: 'createElement',
},
},
},
],
};
20 changes: 19 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@
"presets": [
"@wordpress/default"
],
"plugins": [
[
"./packages/babel-plugin-import-jsx-pragma",
{
"scopeVariable": "createElement",
"source": "@wordpress/element",
"isDefault": false
}
],
[
"babel-plugin-transform-react-jsx",
{
"pragma": "createElement"
}
]
],
"env": {
"production": {
"plugins": [
Expand Down Expand Up @@ -143,7 +159,9 @@
},
"scripts": {
"prebuild": "npm run check-engines",
"build:packages": "rimraf ./packages/*/build ./packages/*/build-module && node ./bin/packages/build.js",
"clean:packages": "rimraf ./packages/*/build ./packages/*/build-module",
"prebuild:packages": "npm run clean:packages && INCLUDE_PACKAGES=babel-plugin-import-jsx-pragma node ./bin/packages/build.js",
"build:packages": "TRANSFORM_JSX_PRAGMA=1 EXCLUDE_PACKAGES=babel-plugin-import-jsx-pragma node ./bin/packages/build.js",
"build": "npm run build:packages && cross-env NODE_ENV=production webpack",
"check-engines": "check-node-version --package",
"ci": "concurrently \"npm run lint && npm run build\" \"npm run test-unit:coverage-ci\"",
Expand Down
1 change: 1 addition & 0 deletions packages/babel-plugin-import-jsx-pragma/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
74 changes: 74 additions & 0 deletions packages/babel-plugin-import-jsx-pragma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
Babel Plugin Import JSX Pragma
======

Babel transform plugin for automatically injecting an import to be used as the pragma for the [React JSX Transform plugin](http://babeljs.io/docs/en/babel-plugin-transform-react-jsx).

[JSX](https://reactjs.org/docs/jsx-in-depth.html) is merely a syntactic sugar for a function call, typically to `React.createElement` when used with [React](https://reactjs.org/). As such, it requires that the function referenced by this transform be within the scope of the file where the JSX occurs. In a typical React project, this means React must be imported in any file where JSX exists.

**Babel Plugin Import JSX Pragma** automates this process by introducing the necessary import automatically wherever JSX exists, allowing you to use JSX in your code without thinking to ensure the transformed function is within scope.

## Installation

Install the module to your project using [npm](https://www.npmjs.com/).

```bash
npm install @wordpress/babel-plugin-import-jsx-pragma
```

## Usage

Refer to the [Babel Plugins documentation](http://babeljs.io/docs/en/plugins) if you don't yet have experience working with Babel plugins.

Include `@wordpress/babel-plugin-import-jsx-pragma` (and [@babel/transform-react-jsx](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx/)) as plugins in your Babel configuration. If you don't include both you will receive errors when encountering JSX tokens.

```js
// .babelrc.js
module.exports = {
plugins: [
'@wordpress/babel-plugin-import-jsx-pragma',
'@babel/transform-react-jsx',
],
};
```

## Options

As the `@babel/transform-react-jsx` plugin offers options to customize the `pragma` to which the transform references, there are equivalent options to assign for customizing the imports generated.

For example, if you are using the `@wordpress/element` package, you may want to use the following configuration:

```js
// .babelrc.js
module.exports = {
plugins: [
[ '@wordpress/babel-plugin-import-jsx-pragma', {
scopeVariable: 'createElement',
source: '@wordpress/element',
isDefault: false,
} ],
[ '@babel/transform-react-jsx', {
pragma: 'createElement',
} ],
],
};
```

### `scopeVariable`

_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.

### `source`

_Type:_ String

The module from which the scope variable is to be imported when missing.

### `isDefautl`

_Type:_ Boolean

Whether the scopeVariable is the default import of the source module.

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
31 changes: 31 additions & 0 deletions packages/babel-plugin-import-jsx-pragma/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@wordpress/babel-plugin-import-jsx-pragma",
"version": "1.0.0-alpha.1",
"description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"babel-plugin",
"jsx",
"pragma",
"react"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/babel-plugin-import-jsx-pragma/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"main": "build/index.js",
"module": "build-module/index.js",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-plugin-syntax-jsx": "^6.18.0"
},
"publishConfig": {
"access": "public"
}
}
102 changes: 102 additions & 0 deletions packages/babel-plugin-import-jsx-pragma/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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.
*/
const DEFAULT_OPTIONS = {
scopeVariable: 'React',
source: 'react',
isDefault: true,
};

/**
* Babel transform plugin for automatically injecting an import to be used as
* the pragma for the React JSX Transform plugin.
*
* @see http://babeljs.io/docs/en/babel-plugin-transform-react-jsx
*
* @param {Object} babel Babel instance.
*
* @return {Object} Babel transform plugin.
*/
export default function( babel ) {
const { types: t } = babel;

function getOptions( state ) {
if ( ! state._options ) {
state._options = {
...DEFAULT_OPTIONS,
...state.opts,
};
}

return state._options;
}

return {
visitor: {
JSXElement( path, state ) {
state.hasJSX = true;
},
ImportDeclaration( path, state ) {
if ( state.hasImportedScopeVariable ) {
return;
}

const { scopeVariable, isDefault } = getOptions( state );

// Test that at least one import specifier exists matching the
// scope variable name. The module source is not verfied since
// we must avoid introducing a conflicting import name, even if
// the scope variable is referenced from a different source.
state.hasImportedScopeVariable = path.node.specifiers.some( ( specifier ) => {
switch ( specifier.type ) {
case 'ImportSpecifier':
return (
! isDefault &&
specifier.imported.name === scopeVariable
);

case 'ImportDefaultSpecifier':
return isDefault;
}
} );
},
Program: {
exit( path, state ) {
if ( ! state.hasJSX || state.hasImportedScopeVariable ) {
return;
}

const { scopeVariable, source, isDefault } = getOptions( state );

let specifier;
if ( isDefault ) {
specifier = t.importDefaultSpecifier(
t.identifier( scopeVariable )
);
} else {
specifier = t.importSpecifier(
t.identifier( scopeVariable ),
t.identifier( scopeVariable )
);
}

const importDeclaration = t.importDeclaration(
[ specifier ],
t.stringLiteral( source )
);

path.unshiftContainer( 'body', importDeclaration );
},
},
},
};
}
Loading

0 comments on commit 6a52599

Please sign in to comment.