diff --git a/vendor/fbtransform/transforms/__tests__/react-test.js b/vendor/fbtransform/transforms/__tests__/react-test.js
index 546db630d7..574643bcd4 100644
--- a/vendor/fbtransform/transforms/__tests__/react-test.js
+++ b/vendor/fbtransform/transforms/__tests__/react-test.js
@@ -30,6 +30,26 @@ describe('react jsx', function() {
);
};
+ // These are placeholder variables in scope that we can use to assert that a
+ // specific variable reference was passed, rather than an object clone of it.
+ var x = 123456;
+ var y = 789012;
+ var z = 345678;
+
+ var HEADER =
+ '/**\n' +
+ ' * @jsx React.DOM\n' +
+ ' */\n';
+
+ var expectObjectAssign = function(code) {
+ var Component = jest.genMockFunction();
+ var Child = jest.genMockFunction();
+ var objectAssignMock = jest.genMockFunction();
+ Object.assign = objectAssignMock;
+ eval(transform(HEADER + code).code);
+ return expect(objectAssignMock);
+ }
+
it('should convert simple tags', function() {
var code = [
'/**@jsx React.DOM*/',
@@ -357,4 +377,54 @@ describe('react jsx', function() {
expect(() => transform(code)).toThrow();
});
+ it('wraps props in Object.assign for spread attributes', function() {
+ var code = HEADER +
+ '';
+ var result = HEADER +
+ 'Component(Object.assign({}, x , {y: \n2, z: true}))';
+ expect(transform(code).code).toBe(result);
+ });
+
+ it('does not call Object.assign when there are no spreads', function() {
+ expectObjectAssign(
+ ''
+ ).not.toBeCalled();
+ });
+
+ it('calls assign with a new target object for spreads', function() {
+ expectObjectAssign(
+ ''
+ ).toBeCalledWith({}, x);
+ });
+
+ it('calls assign with an empty object when the spread is first', function() {
+ expectObjectAssign(
+ ''
+ ).toBeCalledWith({}, x, { y: 2 });
+ });
+
+ it('coalesces consecutive properties into a single object', function() {
+ expectObjectAssign(
+ ''
+ ).toBeCalledWith({}, x, { y: 2, z: true });
+ });
+
+ it('avoids an unnecessary empty object when spread is not first', function() {
+ expectObjectAssign(
+ ''
+ ).toBeCalledWith({x: 1}, y);
+ });
+
+ it('passes the same value multiple times to Object.assign', function() {
+ expectObjectAssign(
+ ''
+ ).toBeCalledWith({x: 1, y: "2"}, z, z);
+ });
+
+ it('evaluates sequences before passing them to Object.assign', function() {
+ expectObjectAssign(
+ 'Text'
+ ).toBeCalledWith({x: "1"}, { y: 2 }, {z: 3});
+ });
+
});
diff --git a/vendor/fbtransform/transforms/react.js b/vendor/fbtransform/transforms/react.js
index c79eaa85f5..be9395653b 100644
--- a/vendor/fbtransform/transforms/react.js
+++ b/vendor/fbtransform/transforms/react.js
@@ -49,6 +49,14 @@ var JSX_ATTRIBUTE_TRANSFORMS = {
}
};
+/**
+ * Removes all non-whitespace/parenthesis characters
+ */
+var reNonWhiteParen = /([^\s\(\)])/g;
+function stripNonWhiteParen(value) {
+ return value.replace(reNonWhiteParen, '');
+}
+
function visitReactTag(traverse, object, path, state) {
var jsxObjIdent = utils.getDocblock(state).jsx;
var openingElement = object.openingElement;
@@ -57,6 +65,7 @@ function visitReactTag(traverse, object, path, state) {
utils.catchup(openingElement.range[0], state, trimLeft);
+
if (nameObject.type === Syntax.XJSNamespacedName && nameObject.namespace) {
throw new Error('Namespace tags are not supported. ReactJSX is not XML.');
}
@@ -75,23 +84,74 @@ function visitReactTag(traverse, object, path, state) {
var hasAttributes = attributesObject.length;
+ var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) {
+ return attr.type === Syntax.XJSSpreadAttribute;
+ });
+
// if we don't have any attributes, pass in null
- if (hasAttributes) {
+ if (hasAtLeastOneSpreadProperty) {
+ utils.append('Object.assign({', state);
+ } else if (hasAttributes) {
utils.append('{', state);
} else {
utils.append('null', state);
}
+ // keep track of if the previous attribute was a spread attribute
+ var previousWasSpread = false;
+
// write attributes
attributesObject.forEach(function(attr, index) {
+ var isLast = index === attributesObject.length - 1;
+
+ if (attr.type === Syntax.XJSSpreadAttribute) {
+ // Plus 1 to skip `{`.
+ utils.move(attr.range[0] + 1, state);
+
+ // Close the previous object or initial object
+ if (!previousWasSpread) {
+ utils.append('}, ', state);
+ }
+
+ // Move to the expression start, ignoring everything except parenthesis
+ // and whitespace.
+ utils.catchup(attr.argument.range[0], state, stripNonWhiteParen);
+
+ traverse(attr.argument, path, state);
+
+ utils.catchup(attr.argument.range[1], state);
+
+ // Move to the end, ignoring parenthesis and the closing `}`
+ utils.catchup(attr.range[1] - 1, state, stripNonWhiteParen);
+
+ if (!isLast) {
+ utils.append(', ', state);
+ }
+
+ utils.move(attr.range[1], state);
+
+ previousWasSpread = true;
+
+ return;
+ }
+
+ // If the next attribute is a spread, we're effective last in this object
+ if (!isLast) {
+ isLast = attributesObject[index + 1].type === Syntax.XJSSpreadAttribute;
+ }
+
if (attr.name.namespace) {
throw new Error(
'Namespace attributes are not supported. ReactJSX is not XML.');
}
var name = attr.name.name;
- var isLast = index === attributesObject.length - 1;
utils.catchup(attr.range[0], state, trimLeft);
+
+ if (previousWasSpread) {
+ utils.append('{', state);
+ }
+
utils.append(quoteAttrName(name), state);
utils.append(': ', state);
@@ -119,6 +179,9 @@ function visitReactTag(traverse, object, path, state) {
}
utils.catchup(attr.range[1], state, trimLeft);
+
+ previousWasSpread = false;
+
});
if (!openingElement.selfClosing) {
@@ -126,10 +189,14 @@ function visitReactTag(traverse, object, path, state) {
utils.move(openingElement.range[1], state);
}
- if (hasAttributes) {
+ if (hasAttributes && !previousWasSpread) {
utils.append('}', state);
}
+ if (hasAtLeastOneSpreadProperty) {
+ utils.append(')', state);
+ }
+
// filter out whitespace
var childrenToRender = object.children.filter(function(child) {
return !(child.type === Syntax.Literal