Skip to content
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

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

Merged
merged 5 commits into from
Jun 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not sure why we need all these env variables (this one and the include/exclude ones)
Can't we just always use this plugin? What's the issue if always include it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see so I guess this would solve the issue right? #7556

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should include this in the babel preset package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 😄 It's been an awkward struggle thus far with the preset. I imagine it will be made easier once it's part of Gutenberg proper.


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