Sync out latest JSX transforms

The biggest change: instead of calling functions directly, we now use
React.createElement to wrap.

The other big change: this removes the need for the @jsx pragma.
Instead we'll parse everything, assume React is in scope,
and only look for the actual XJS parts in the AST (as opposed to only
runing the transform when @jsx was specified in the docblock).

Note: this is actually a series of commits internally and should have
been syned out in pieces over the past several weeks.
This commit is contained in:
Paul O’Shannessy 2014-08-20 00:24:26 -07:00
parent aae31ae24c
commit 2058ad99be
4 changed files with 72 additions and 104 deletions

View File

@ -29,23 +29,21 @@ describe('react displayName jsx', function() {
it('should only inject displayName if missing', function() { it('should only inject displayName if missing', function() {
var code = [ var code = [
'/** @jsx React.DOM */',
'"use strict";', '"use strict";',
'var Whateva = React.createClass({', 'var Whateva = React.createClass({',
' displayName: \'Whateva\',', ' displayName: \'Whateva\',',
' render: function() {', ' render: function() {',
' return <div className=\'whateva\'>...whateva.</div>;', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
var result = [ var result = [
'/** @jsx React.DOM */',
'"use strict";', '"use strict";',
'var Whateva = React.createClass({', 'var Whateva = React.createClass({',
' displayName: \'Whateva\',', ' displayName: \'Whateva\',',
' render: function() {', ' render: function() {',
' return React.DOM.div({className: "whateva"}, "...whateva.");', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
@ -55,19 +53,17 @@ describe('react displayName jsx', function() {
it('should inject displayName in simple assignment', () => { it('should inject displayName in simple assignment', () => {
var code = [ var code = [
'/** @jsx React.DOM */',
'var Component = React.createClass({', 'var Component = React.createClass({',
' render: function() {', ' render: function() {',
' return <div/>;', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
var result = [ var result = [
'/** @jsx React.DOM */',
'var Component = React.createClass({displayName: \'Component\',', 'var Component = React.createClass({displayName: \'Component\',',
' render: function() {', ' render: function() {',
' return React.DOM.div(null);', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
@ -77,21 +73,19 @@ describe('react displayName jsx', function() {
it('should inject displayName in simple assignment without var', () => { it('should inject displayName in simple assignment without var', () => {
var code = [ var code = [
'/** @jsx React.DOM */',
'var Component;', 'var Component;',
'Component = React.createClass({', 'Component = React.createClass({',
' render: function() {', ' render: function() {',
' return <div/>;', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
var result = [ var result = [
'/** @jsx React.DOM */',
'var Component;', 'var Component;',
'Component = React.createClass({displayName: \'Component\',', 'Component = React.createClass({displayName: \'Component\',',
' render: function() {', ' render: function() {',
' return React.DOM.div(null);', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
@ -101,19 +95,17 @@ describe('react displayName jsx', function() {
it('should inject displayName in property assignment', () => { it('should inject displayName in property assignment', () => {
var code = [ var code = [
'/** @jsx React.DOM */',
'exports.Component = React.createClass({', 'exports.Component = React.createClass({',
' render: function() {', ' render: function() {',
' return <div/>;', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
var result = [ var result = [
'/** @jsx React.DOM */',
'exports.Component = React.createClass({displayName: \'Component\',', 'exports.Component = React.createClass({displayName: \'Component\',',
' render: function() {', ' render: function() {',
' return React.DOM.div(null);', ' return null;',
' }', ' }',
'});' '});'
].join('\n'); ].join('\n');
@ -123,22 +115,20 @@ describe('react displayName jsx', function() {
it('should inject displayName in object declaration', () => { it('should inject displayName in object declaration', () => {
var code = [ var code = [
'/** @jsx React.DOM */',
'exports = {', 'exports = {',
' Component: React.createClass({', ' Component: React.createClass({',
' render: function() {', ' render: function() {',
' return <div/>;', ' return null;',
' }', ' }',
' })', ' })',
'};' '};'
].join('\n'); ].join('\n');
var result = [ var result = [
'/** @jsx React.DOM */',
'exports = {', 'exports = {',
' Component: React.createClass({displayName: \'Component\',', ' Component: React.createClass({displayName: \'Component\',',
' render: function() {', ' render: function() {',
' return React.DOM.div(null);', ' return null;',
' }', ' }',
' })', ' })',
'};' '};'

View File

@ -15,6 +15,9 @@
* *
* @emails jeffmo@fb.com * @emails jeffmo@fb.com
*/ */
/*jshint evil:true, unused:false*/
"use strict"; "use strict";
require('mock-modules').autoMockOff(); require('mock-modules').autoMockOff();
@ -42,20 +45,19 @@ describe('react jsx', function() {
var y = 789012; var y = 789012;
var z = 345678; var z = 345678;
var HEADER =
'/**\n' +
' * @jsx React.DOM\n' +
' */\n';
var expectObjectAssign = function(code) { var expectObjectAssign = function(code) {
var Component = jest.genMockFunction(); var Component = jest.genMockFunction();
var Child = jest.genMockFunction(); var Child = jest.genMockFunction();
var objectAssignMock = jest.genMockFunction(); var objectAssignMock = jest.genMockFunction();
Object.assign = objectAssignMock; Object.assign = objectAssignMock;
eval(transform(HEADER + code).code); eval(transform(code).code);
return expect(objectAssignMock); return expect(objectAssignMock);
} }
var React = {
createElement: jest.genMockFunction()
};
it('should convert simple tags', function() { it('should convert simple tags', function() {
var code = [ var code = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
@ -63,7 +65,7 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
var result = [ var result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = React.DOM.div(null);' 'var x = React.createElement(React.DOM.div, null);'
].join('\n'); ].join('\n');
expect(transform(code).code).toEqual(result); expect(transform(code).code).toEqual(result);
@ -76,7 +78,7 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
var result = [ var result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = React.DOM.div(null, "text");' 'var x = React.createElement(React.DOM.div, null, "text");'
].join('\n'); ].join('\n');
expect(transform(code).code).toEqual(result); expect(transform(code).code).toEqual(result);
@ -93,10 +95,12 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
var result = [ var result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = React.DOM.div(null, ', 'var x = React.createElement(React.DOM.div, null, ',
' React.DOM.div(null, React.DOM.br(null)), ', ' React.createElement(React.DOM.div, null, ' +
' Component(null, foo, React.DOM.br(null), bar), ', 'React.createElement(React.DOM.br, null)), ',
' React.DOM.br(null)', ' React.createElement(Component, null, foo, ' +
'React.createElement(React.DOM.br, null), bar), ',
' React.createElement(React.DOM.br, null)',
');' ');'
].join('\n'); ].join('\n');
@ -113,8 +117,8 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
var result = [ var result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = React.DOM.div(null, ', 'var x = React.createElement(React.DOM.div, null, ',
' Component(null)', ' React.createElement(Component, null)',
');' ');'
].join('\n'); ].join('\n');
@ -129,7 +133,7 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
result = [ result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = React.DOM.div(null, ', 'var x = React.createElement(React.DOM.div, null, ',
' this.props.children', ' this.props.children',
');' ');'
].join('\n'); ].join('\n');
@ -144,7 +148,7 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
result = [ result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = Composite(null, ', 'var x = React.createElement(Composite, null, ',
' this.props.children', ' this.props.children',
');' ');'
].join('\n'); ].join('\n');
@ -159,8 +163,8 @@ describe('react jsx', function() {
].join('\n'); ].join('\n');
result = [ result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x = Composite(null, ', 'var x = React.createElement(Composite, null, ',
' Composite2(null)', ' React.createElement(Composite2, null)',
');' ');'
].join('\n'); ].join('\n');
expect(transform(code).code).toEqual(result); expect(transform(code).code).toEqual(result);
@ -190,7 +194,7 @@ describe('react jsx', function() {
var result = [ var result = [
'/**@jsx React.DOM*/', '/**@jsx React.DOM*/',
'var x =', 'var x =',
' React.DOM.div({', ' React.createElement(React.DOM.div, {',
' attr1: ', ' attr1: ',
' "foo" + "bar", ', ' "foo" + "bar", ',
' ', ' ',
@ -235,14 +239,14 @@ describe('react jsx', function() {
' * @jsx React.DOM', ' * @jsx React.DOM',
' */', ' */',
'var x = (', 'var x = (',
' React.DOM.div(null, ', ' React.createElement(React.DOM.div, null, ',
' /* A comment at the beginning */', ' /* A comment at the beginning */',
' /* A second comment at the beginning */', ' /* A second comment at the beginning */',
' React.DOM.span(null', ' React.createElement(React.DOM.span, null',
' /* A nested comment */', ' /* A nested comment */',
' ), ', ' ), ',
' /* A sandwiched comment */', ' /* A sandwiched comment */',
' React.DOM.br(null)', ' React.createElement(React.DOM.br, null)',
' /* A comment at the end */', ' /* A comment at the end */',
' /* A second comment at the end */', ' /* A second comment at the end */',
' )', ' )',
@ -273,11 +277,11 @@ describe('react jsx', function() {
' * @jsx React.DOM', ' * @jsx React.DOM',
' */', ' */',
'var x = (', 'var x = (',
' React.DOM.div({', ' React.createElement(React.DOM.div, {',
' /* a multi-line', ' /* a multi-line',
' comment */', ' comment */',
' attr1: "foo"}, ', ' attr1: "foo"}, ',
' React.DOM.span({// a double-slash comment', ' React.createElement(React.DOM.span, {// a double-slash comment',
' attr2: "bar"}', ' attr2: "bar"}',
' )', ' )',
' )', ' )',
@ -298,7 +302,7 @@ describe('react jsx', function() {
'/**', '/**',
' * @jsx React.DOM', ' * @jsx React.DOM',
' */', ' */',
'React.DOM.div(null, "\u00A0");' 'React.createElement(React.DOM.div, null, "\u00A0");'
].join('\n'); ].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
@ -315,59 +319,29 @@ describe('react jsx', function() {
'/**', '/**',
' * @jsx React.DOM', ' * @jsx React.DOM',
' */', ' */',
'React.DOM.div(null, "\u00A0 ");' 'React.createElement(React.DOM.div, null, "\u00A0 ");'
].join('\n'); ].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
}); });
it('should handle hasOwnProperty correctly', function() { it('should handle hasOwnProperty correctly', function() {
var code = [ var code = '<hasOwnProperty>testing</hasOwnProperty>;';
'/**', var result = 'React.createElement(hasOwnProperty, null, "testing");';
' * @jsx React.DOM',
' */',
'<hasOwnProperty>testing</hasOwnProperty>;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'hasOwnProperty(null, "testing");'
].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
}); });
it('should allow constructor as prop', function() { it('should allow constructor as prop', function() {
var code = [ var code = '<Component constructor="foo" />;';
'/**', var result = 'React.createElement(Component, {constructor: "foo"});';
' * @jsx React.DOM',
' */',
'<Component constructor="foo" />;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'Component({constructor: "foo"});'
].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
}); });
it('should allow JS namespacing', function() { it('should allow JS namespacing', function() {
var code = [ var code = '<Namespace.Component />;';
'/**', var result = 'React.createElement(Namespace.Component, null);';
' * @jsx React.DOM',
' */',
'<Namespace.Component />;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'Namespace.Component(null);'
].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
}); });
@ -383,7 +357,7 @@ describe('react jsx', function() {
'/**', '/**',
' * @jsx React.DOM', ' * @jsx React.DOM',
' */', ' */',
'Namespace.DeepNamespace.Component(null);' 'React.createElement(Namespace.DeepNamespace.Component, null);'
].join('\n'); ].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
@ -401,10 +375,12 @@ describe('react jsx', function() {
}); });
it('wraps props in Object.assign for spread attributes', function() { it('wraps props in Object.assign for spread attributes', function() {
var code = HEADER + var code =
'<Component { ... x } y\n={2 } z />'; '<Component { ... x } y\n' +
var result = HEADER + '={2 } z />';
'Component(Object.assign({}, x , {y: \n2, z: true}))'; var result =
'React.createElement(Component, Object.assign({}, x , {y: \n' +
'2, z: true}))';
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);
}); });
@ -420,7 +396,7 @@ describe('react jsx', function() {
'/**', '/**',
' * @jsx React.DOM', ' * @jsx React.DOM',
' */', ' */',
'React.DOM[\'font-face\'](null);' 'React.createElement(React.DOM[\'font-face\'], null);'
].join('\n'); ].join('\n');
expect(transform(code).code).toBe(result); expect(transform(code).code).toBe(result);

View File

@ -69,6 +69,13 @@ function visitReactTag(traverse, object, path, state) {
throw new Error('Namespace tags are not supported. ReactJSX is not XML.'); throw new Error('Namespace tags are not supported. ReactJSX is not XML.');
} }
var isReact = jsxObjIdent !== 'JSXDOM';
// We assume that the React runtime is already in scope
if (isReact) {
utils.append('React.createElement(', state);
}
// Only identifiers can be fallback tags or need quoting. We don't need to // Only identifiers can be fallback tags or need quoting. We don't need to
// handle quoting for other types. // handle quoting for other types.
var didAddTag = false; var didAddTag = false;
@ -106,7 +113,11 @@ function visitReactTag(traverse, object, path, state) {
utils.catchup(nameObject.range[1], state); utils.catchup(nameObject.range[1], state);
} }
if (isReact) {
utils.append(', ', state);
} else {
utils.append('(', state); utils.append('(', state);
}
var hasAttributes = attributesObject.length; var hasAttributes = attributesObject.length;
@ -278,9 +289,7 @@ function visitReactTag(traverse, object, path, state) {
} }
visitReactTag.test = function(object, path, state) { visitReactTag.test = function(object, path, state) {
// only run react when react @jsx namespace is specified in docblock return object.type === Syntax.XJSElement;
var jsx = utils.getDocblock(state).jsx;
return object.type === Syntax.XJSElement && jsx && jsx.length;
}; };
exports.visitorList = [ exports.visitorList = [

View File

@ -87,19 +87,12 @@ function visitReactDisplayName(traverse, object, path, state) {
} }
} }
/**
* Will only run on @jsx files for now.
*/
visitReactDisplayName.test = function(object, path, state) { visitReactDisplayName.test = function(object, path, state) {
if (utils.getDocblock(state).jsx) {
return ( return (
object.type === Syntax.AssignmentExpression || object.type === Syntax.AssignmentExpression ||
object.type === Syntax.Property || object.type === Syntax.Property ||
object.type === Syntax.VariableDeclarator object.type === Syntax.VariableDeclarator
); );
} else {
return false;
}
}; };
exports.visitorList = [ exports.visitorList = [