Update React JSX Transforms

* Extract JSXDOM into separate transform
* Rename lower case variables in JSX to upper case
* Drop React.DOM docblock, use string tags instead
This commit is contained in:
Sebastian Markbage 2014-10-07 10:44:39 -07:00 committed by Paul O’Shannessy
parent 1a1104f56f
commit c4658c1728
2 changed files with 61 additions and 172 deletions

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @emails jeffmo@fb.com
* @emails react-core
*/
/*jshint evil:true, unused:false*/
@ -59,34 +59,21 @@ describe('react jsx', function() {
};
it('should convert simple tags', function() {
var code = [
'/**@jsx React.DOM*/',
'var x = <div></div>;'
].join('\n');
var result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(React.DOM.div, null);'
].join('\n');
var code = 'var x = <div></div>;';
var result = 'var x = React.createElement("div", null);';
expect(transform(code).code).toEqual(result);
});
it('should convert simple text', function() {
var code = [
'/**@jsx React.DOM*/\n' +
'var x = <div>text</div>;'
].join('\n');
var result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(React.DOM.div, null, "text");'
].join('\n');
var code = 'var x = <div>text</div>;';
var result = 'var x = React.createElement("div", null, "text");';
expect(transform(code).code).toEqual(result);
});
it('should have correct comma in nested children', function() {
var code = [
'/**@jsx React.DOM*/',
'var x = <div>',
' <div><br /></div>',
' <Component>{foo}<br />{bar}</Component>',
@ -94,13 +81,12 @@ describe('react jsx', function() {
'</div>;'
].join('\n');
var result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(React.DOM.div, null, ',
' React.createElement(React.DOM.div, null, ' +
'React.createElement(React.DOM.br, null)), ',
'var x = React.createElement("div", null, ',
' React.createElement("div", null, ' +
'React.createElement("br", null)), ',
' React.createElement(Component, null, foo, ' +
'React.createElement(React.DOM.br, null), bar), ',
' React.createElement(React.DOM.br, null)',
'React.createElement("br", null), bar), ',
' React.createElement("br", null)',
');'
].join('\n');
@ -110,14 +96,12 @@ describe('react jsx', function() {
it('should avoid wrapping in extra parens if not needed', function() {
// Try with a single composite child, wrapped in a div.
var code = [
'/**@jsx React.DOM*/',
'var x = <div>',
' <Component />',
'</div>;'
].join('\n');
var result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(React.DOM.div, null, ',
'var x = React.createElement("div", null, ',
' React.createElement(Component, null)',
');'
].join('\n');
@ -126,14 +110,12 @@ describe('react jsx', function() {
// Try with a single interpolated child, wrapped in a div.
code = [
'/**@jsx React.DOM*/',
'var x = <div>',
' {this.props.children}',
'</div>;'
].join('\n');
result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(React.DOM.div, null, ',
'var x = React.createElement("div", null, ',
' this.props.children',
');'
].join('\n');
@ -141,13 +123,11 @@ describe('react jsx', function() {
// Try with a single interpolated child, wrapped in a composite.
code = [
'/**@jsx React.DOM*/',
'var x = <Composite>',
' {this.props.children}',
'</Composite>;'
].join('\n');
result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(Composite, null, ',
' this.props.children',
');'
@ -156,13 +136,11 @@ describe('react jsx', function() {
// Try with a single composite child, wrapped in a composite.
code = [
'/**@jsx React.DOM*/',
'var x = <Composite>',
' <Composite2 />',
'</Composite>;'
].join('\n');
result = [
'/**@jsx React.DOM*/',
'var x = React.createElement(Composite, null, ',
' React.createElement(Composite2, null)',
');'
@ -172,7 +150,6 @@ describe('react jsx', function() {
it('should insert commas after expressions before whitespace', function() {
var code = [
'/**@jsx React.DOM*/',
'var x =',
' <div',
' attr1={',
@ -192,9 +169,8 @@ describe('react jsx', function() {
' </div>;'
].join('\n');
var result = [
'/**@jsx React.DOM*/',
'var x =',
' React.createElement(React.DOM.div, {',
' React.createElement("div", {',
' attr1: ',
' "foo" + "bar", ',
' ',
@ -217,9 +193,6 @@ describe('react jsx', function() {
it('should properly handle comments adjacent to children', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'var x = (',
' <div>',
' {/* A comment at the beginning */}',
@ -235,18 +208,15 @@ describe('react jsx', function() {
');'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'var x = (',
' React.createElement(React.DOM.div, null, ',
' React.createElement("div", null, ',
' /* A comment at the beginning */',
' /* A second comment at the beginning */',
' React.createElement(React.DOM.span, null',
' React.createElement("span", null',
' /* A nested comment */',
' ), ',
' /* A sandwiched comment */',
' React.createElement(React.DOM.br, null)',
' React.createElement("br", null)',
' /* A comment at the end */',
' /* A second comment at the end */',
' )',
@ -258,9 +228,6 @@ describe('react jsx', function() {
it('should properly handle comments between props', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'var x = (',
' <div',
' /* a multi-line',
@ -273,15 +240,12 @@ describe('react jsx', function() {
');'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'var x = (',
' React.createElement(React.DOM.div, {',
' React.createElement("div", {',
' /* a multi-line',
' comment */',
' attr1: "foo"}, ',
' React.createElement(React.DOM.span, {// a double-slash comment',
' React.createElement("span", {// a double-slash comment',
' attr2: "bar"}',
' )',
' )',
@ -293,16 +257,10 @@ describe('react jsx', function() {
it('should not strip tags with a single child of &nbsp;', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'<div>&nbsp;</div>;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'React.createElement(React.DOM.div, null, "\u00A0");'
'React.createElement("div", null, "\u00A0");'
].join('\n');
expect(transform(code).code).toBe(result);
@ -310,16 +268,10 @@ describe('react jsx', function() {
it('should not strip &nbsp; even coupled with other whitespace', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'<div>&nbsp; </div>;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'React.createElement(React.DOM.div, null, "\u00A0 ");'
'React.createElement("div", null, "\u00A0 ");'
].join('\n');
expect(transform(code).code).toBe(result);
@ -327,9 +279,13 @@ describe('react jsx', function() {
it('should handle hasOwnProperty correctly', function() {
var code = '<hasOwnProperty>testing</hasOwnProperty>;';
var result = 'React.createElement(hasOwnProperty, null, "testing");';
// var result = 'React.createElement("hasOwnProperty", null, "testing");';
expect(transform(code).code).toBe(result);
// expect(transform(code).code).toBe(result);
// This is currently not supported, and will generate a string tag in
// a follow up.
expect(() => transform(code)).toThrow();
});
it('should allow constructor as prop', function() {
@ -347,29 +303,15 @@ describe('react jsx', function() {
});
it('should allow deeper JS namespacing', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'<Namespace.DeepNamespace.Component />;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'React.createElement(Namespace.DeepNamespace.Component, null);'
].join('\n');
var code = '<Namespace.DeepNamespace.Component />;';
var result =
'React.createElement(Namespace.DeepNamespace.Component, null);';
expect(transform(code).code).toBe(result);
});
it('should disallow XML namespacing', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'<Namespace:Component />;'
].join('\n');
var code = '<Namespace:Component />;';
expect(() => transform(code)).toThrow();
});
@ -399,18 +341,8 @@ describe('react jsx', function() {
});
it('should transform known hyphenated tags', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'<font-face />;'
].join('\n');
var result = [
'/**',
' * @jsx React.DOM',
' */',
'React.createElement(React.DOM[\'font-face\'], null);'
].join('\n');
var code = '<font-face />;';
var result = 'React.createElement("font-face", null);';
expect(transform(code).code).toBe(result);
});
@ -422,12 +354,7 @@ describe('react jsx', function() {
});
it('should throw for unknown hyphenated tags', function() {
var code = [
'/**',
' * @jsx React.DOM',
' */',
'<x-component />;'
].join('\n');
var code = '<x-component />;';
expect(() => transform(code)).toThrow();
});

View File

@ -28,27 +28,16 @@ var quoteAttrName = require('./xjs').quoteAttrName;
var trimLeft = require('./xjs').trimLeft;
/**
* Customized desugar processor.
* Customized desugar processor for React JSX. Currently:
*
* Currently: (Somewhat tailored to React)
* <X> </X> => X(null, null)
* <X prop="1" /> => X({prop: '1'}, null)
* <X prop="2"><Y /></X> => X({prop:'2'}, Y(null, null))
* <X prop="2"><Y /><Z /></X> => X({prop:'2'}, [Y(null, null), Z(null, null)])
*
* Exceptions to the simple rules above:
* if a property is named "class" it will be changed to "className" in the
* javascript since "class" is not a valid object key in javascript.
* <X> </X> => React.createElement(X, null)
* <X prop="1" /> => React.createElement(X, {prop: '1'}, null)
* <X prop="2"><Y /></X> => React.createElement(X, {prop:'2'},
* React.createElement(Y, null)
* )
* <div /> => React.createElement("div", null)
*/
var JSX_ATTRIBUTE_TRANSFORMS = {
cxName: function(attr) {
throw new Error(
"cxName is no longer supported, use className={cx(...)} instead"
);
}
};
/**
* Removes all non-whitespace/parenthesis characters
*/
@ -57,8 +46,12 @@ function stripNonWhiteParen(value) {
return value.replace(reNonWhiteParen, '');
}
var tagConvention = /^[a-z]|\-/;
function isTagName(name) {
return tagConvention.test(name);
}
function visitReactTag(traverse, object, path, state) {
var jsxObjIdent = utils.getDocblock(state).jsx;
var openingElement = object.openingElement;
var nameObject = openingElement.name;
var attributesObject = openingElement.attributes;
@ -69,55 +62,31 @@ function visitReactTag(traverse, object, path, state) {
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);
}
utils.append('React.createElement(', state);
// Only identifiers can be fallback tags or need quoting. We don't need to
// handle quoting for other types.
var didAddTag = false;
// Only identifiers can be fallback tags. XJSMemberExpressions are not.
if (nameObject.type === Syntax.XJSIdentifier) {
var tagName = nameObject.name;
var quotedTagName = quoteAttrName(tagName);
if (FALLBACK_TAGS.hasOwnProperty(tagName)) {
// "Properly" handle invalid identifiers, like <font-face>, which needs to
// be enclosed in quotes.
var predicate =
tagName === quotedTagName ?
('.' + tagName) :
('[' + quotedTagName + ']');
utils.append(jsxObjIdent + predicate, state);
utils.move(nameObject.range[1], state);
didAddTag = true;
} else if (tagName !== quotedTagName) {
// If we're in the case where we need to quote and but don't recognize the
// tag, throw.
// Identifiers with lower case or hypthens are fallback tags (strings).
// XJSMemberExpressions are not.
if (nameObject.type === Syntax.XJSIdentifier && isTagName(nameObject.name)) {
// This is a temporary error message to assist upgrades
if (!FALLBACK_TAGS.hasOwnProperty(nameObject.name)) {
throw new Error(
'Tags must be valid JS identifiers or a recognized special case. `<' +
tagName + '>` is not one of them.'
'Lower case component names (' + nameObject.name + ') are no longer ' +
'supported in JSX: See http://fb.me/react-jsx-lower-case'
);
}
}
// Use utils.catchup in this case so we can easily handle XJSMemberExpressions
// which look like Foo.Bar.Baz. This also handles unhyphenated XJSIdentifiers
// that aren't fallback tags.
if (!didAddTag) {
utils.append('"' + nameObject.name + '"', state);
utils.move(nameObject.range[1], state);
} else {
// Use utils.catchup in this case so we can easily handle
// XJSMemberExpressions which look like Foo.Bar.Baz. This also handles
// XJSIdentifiers that aren't fallback tags.
utils.move(nameObject.range[0], state);
utils.catchup(nameObject.range[1], state);
}
if (isReact) {
utils.append(', ', state);
} else {
utils.append('(', state);
}
utils.append(', ', state);
var hasAttributes = attributesObject.length;
@ -147,7 +116,6 @@ function visitReactTag(traverse, object, path, state) {
utils.append('}, ', state);
}
// Move to the expression start, ignoring everything except parenthesis
// and whitespace.
utils.catchup(attr.range[0], state, stripNonWhiteParen);
@ -203,13 +171,7 @@ function visitReactTag(traverse, object, path, state) {
utils.move(attr.name.range[1], state);
// Use catchupNewlines to skip over the '=' in the attribute
utils.catchupNewlines(attr.value.range[0], state);
if (JSX_ATTRIBUTE_TRANSFORMS.hasOwnProperty(attr.name.name)) {
utils.append(JSX_ATTRIBUTE_TRANSFORMS[attr.name.name](attr), state);
utils.move(attr.value.range[1], state);
if (!isLast) {
utils.append(', ', state);
}
} else if (attr.value.type === Syntax.Literal) {
if (attr.value.type === Syntax.Literal) {
renderXJSLiteral(attr.value, isLast, state);
} else {
renderXJSExpressionContainer(traverse, attr.value, isLast, path, state);