Skip to content

Commit

Permalink
Merge branch 'jsx' into 'master'
Browse files Browse the repository at this point in the history
JSX support

By popular demand...

This implements very basic support for JSX. Still to do:

* Support pragmas other than `React.createElement`
* Optimisations (see [here](https://medium.com/doctolib-engineering/improve-react-performance-with-babel-16f1becfaa25#.thsp2ymcd) and [here](facebook/react#3226))
* Some method to auto-import needed modules? (e.g. automatically add `import * as React from 'react'`

It's still an open question whether this truly belongs in core, but I do like the convenience of it.

Since I don't ever use JSX, I could have completely pooched this up – would welcome input from people more experienced with it.

See merge request !37
  • Loading branch information
Rich-Harris committed May 25, 2016
2 parents fbc24e7 + c3723e7 commit 5007bcf
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 17 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@
"rimraf": "^2.5.2",
"rollup": "^0.26.3",
"rollup-plugin-buble": "^0.8.0",
"rollup-plugin-commonjs": "^2.2.1",
"rollup-plugin-json": "^2.0.0",
"rollup-plugin-node-resolve": "^1.5.0",
"source-map": "^0.5.6",
"source-map-support": "^0.4.0"
},
"dependencies": {
"acorn": "^3.1.0",
"acorn-jsx": "^3.0.1",
"chalk": "^1.1.3",
"magic-string": "^0.14.0",
"minimist": "^1.2.0"
Expand Down
16 changes: 4 additions & 12 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import buble from 'rollup-plugin-buble';
import json from 'rollup-plugin-json';
import nodeResolve from 'rollup-plugin-node-resolve';
import { resolve } from 'path';
import commonjs from 'rollup-plugin-commonjs';

var external = process.env.DEPS ? null : [ 'acorn', 'magic-string' ];
var external = process.env.DEPS ? [] : [ 'acorn-jsx', 'magic-string' ];

export default {
entry: 'src/index.js',
moduleName: 'buble',
plugins: [
{
resolveId: function ( id ) {
// for the browser build, we want to bundle Acorn, but not
// from the dist file
if ( process.env.DEPS && id === 'acorn' ) {
return resolve( __dirname, 'node_modules/acorn/src/index.js' );
}
}
},
json(),
commonjs(),
buble({
include: [ 'src/**', 'node_modules/acorn/**' ],
transforms: {
Expand All @@ -32,7 +24,7 @@ export default {
],
external: external,
globals: {
'acorn': 'acorn',
'acorn-jsx': 'acorn',
'magic-string': 'MagicString'
},
sourceMap: true
Expand Down
9 changes: 5 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parse } from 'acorn';
import acorn from 'acorn-jsx';
import Program from './program/Program.js';
import { features, matrix } from './support.js';
import getSnippet from './utils/getSnippet.js';
Expand Down Expand Up @@ -41,10 +41,11 @@ export function transform ( source, options = {} ) {
let ast;

try {
ast = parse( source, {
ast = acorn.parse( source, {
ecmaVersion: 7,
preserveParens: true,
sourceType: 'module'
sourceType: 'module',
plugins: { jsx: true }
});
} catch ( err ) {
err.snippet = getSnippet( source, err.loc );
Expand All @@ -63,7 +64,7 @@ export function transform ( source, options = {} ) {
transforms[ name ] = options.transforms[ name ];
});

return new Program( source, ast, transforms ).export( options );
return new Program( source, ast, transforms, options ).export( options );
}

export { version as VERSION } from '../package.json';
5 changes: 4 additions & 1 deletion src/program/Program.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import MagicString from 'magic-string';
import BlockStatement from './BlockStatement.js';
import wrap from './wrap.js';

export default function Program ( source, ast, transforms ) {
export default function Program ( source, ast, transforms, options ) {
this.type = 'Root';

// options
this.jsx = options.jsx || 'React.createElement';

this.source = source;
this.magicString = new MagicString( source );

Expand Down
9 changes: 9 additions & 0 deletions src/program/types/JSXAttribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Node from '../Node.js';

export default class JSXAttribute extends Node {
transpile ( code, transforms ) {
code.overwrite( this.name.end, this.value.start, ': ' );

super.transpile( code, transforms );
}
}
7 changes: 7 additions & 0 deletions src/program/types/JSXClosingElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Node from '../Node.js';

export default class JSXClosingElement extends Node {
transpile ( code, transforms ) {
code.remove( this.start, this.end );
}
}
22 changes: 22 additions & 0 deletions src/program/types/JSXElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Node from '../Node.js';

export default class JSXElement extends Node {
transpile ( code, transforms ) {
code.insertLeft( this.end, `)` );

super.transpile( code, transforms );

const children = this.children.filter( child => {
return child.type === 'JSXElement' || /\S/.test( child.value );
});

if ( children.length ) {
code.insertLeft( this.openingElement.end, ',' );

for ( let i = 0; i < children.length - 1; i += 1 ) {
const child = children[i];
code.insertLeft( child.end, ',' );
}
}
}
}
10 changes: 10 additions & 0 deletions src/program/types/JSXExpressionContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Node from '../Node.js';

export default class JSXExpressionContainer extends Node {
transpile ( code, transforms ) {
code.remove( this.start, this.expression.start );
code.remove( this.expression.end, this.end );

super.transpile( code, transforms );
}
}
32 changes: 32 additions & 0 deletions src/program/types/JSXOpeningElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Node from '../Node.js';

export default class JSXOpeningElement extends Node {
transpile ( code, transforms ) {
code.overwrite( this.start, this.name.start, `${this.program.jsx}( ` );

const html = this.name.type === 'JSXIdentifier' && this.name.name[0] === this.name.name[0].toLowerCase();
if ( html ) code.insertRight( this.name.start, `'` );

const len = this.attributes.length;
let c = this.name.end;

if ( len ) {
code.insertLeft( this.name.end, html ? `', {` : `, {` );
code.insertLeft( this.attributes[ len - 1 ].end, ' }' );

let i;
c = this.attributes[0].end;

for ( i = 1; i < len; i += 1 ) {
code.overwrite( c, this.attributes[i].start, ', ' );
c = this.attributes[i].end;
}
} else {
code.insertLeft( this.name.end, `', null` );
c = this.name.end;
}

code.remove( c, this.end );
super.transpile( code, transforms );
}
}
10 changes: 10 additions & 0 deletions src/program/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import Identifier from './Identifier.js';
import ImportDeclaration from './ImportDeclaration.js';
import ImportDefaultSpecifier from './ImportDefaultSpecifier.js';
import ImportSpecifier from './ImportSpecifier.js';
import JSXAttribute from './JSXAttribute.js';
import JSXClosingElement from './JSXClosingElement.js';
import JSXElement from './JSXElement.js';
import JSXExpressionContainer from './JSXExpressionContainer.js';
import JSXOpeningElement from './JSXOpeningElement.js';
import Literal from './Literal.js';
import LoopStatement from './shared/LoopStatement.js';
import MemberExpression from './MemberExpression.js';
Expand Down Expand Up @@ -56,6 +61,11 @@ export default {
ImportDeclaration,
ImportDefaultSpecifier,
ImportSpecifier,
JSXAttribute,
JSXClosingElement,
JSXElement,
JSXExpressionContainer,
JSXOpeningElement,
Literal,
MemberExpression,
Property,
Expand Down
76 changes: 76 additions & 0 deletions test/samples/jsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module.exports = [
{
description: 'transpiles self-closing JSX tag',
input: `var img = <img src='foo.gif'/>;`,
output: `var img = React.createElement( 'img', { src: 'foo.gif' });`
},

{
description: 'transpiles non-self-closing JSX tag',
input: `var div = <div className='foo'></div>;`,
output: `var div = React.createElement( 'div', { className: 'foo' });`
},

{
description: 'transpiles nested JSX tags',

input: `
var div = (
<div className='foo'>
<img src='foo.gif'/>
<img src='bar.gif'/>
</div>
);`,

output: `
var div = (
React.createElement( 'div', { className: 'foo' },
React.createElement( 'img', { src: 'foo.gif' }),
React.createElement( 'img', { src: 'bar.gif' })
)
);`
},

{
description: 'transpiles JSX tag with expression attributes',
input: `var img = <img src={src}/>;`,
output: `var img = React.createElement( 'img', { src: src });`
},

{
description: 'transpiles JSX tag with expression children',

input: `
var div = (
<div>
{ images.map( src => <img src={src}/> ) }
</div>
);`,

output: `
var div = (
React.createElement( 'div', null,
images.map( function (src) { return React.createElement( 'img', { src: src }); } )
)
);`
},

{
description: 'transpiles JSX component',
input: `var element = <Hello name={name}/>;`,
output: `var element = React.createElement( Hello, { name: name });`
},

{
description: 'transpiles namespaced JSX component',
input: `var element = <Foo.Bar name={name}/>;`,
output: `var element = React.createElement( Foo.Bar, { name: name });`
},

{
description: 'supports pragmas',
options: { jsx: 'NotReact.createElement' },
input: `var img = <img src='foo.gif'/>;`,
output: `var img = NotReact.createElement( 'img', { src: 'foo.gif' });`
}
];

0 comments on commit 5007bcf

Please sign in to comment.