mirror of
https://github.com/facebook/react.git
synced 2026-02-24 12:43:00 +00:00
[react jsx transform] Spread attribute -> Object.assign
Add support for spread attributes. Transforms into an Object.assign just like jstransform does for spread properties in object literals. Depends on https://github.com/facebook/esprima/pull/22
This commit is contained in:
committed by
Paul O’Shannessy
parent
0cf686fe1e
commit
e6134c307e
@@ -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 +
|
||||
'<Component { ... x } y\n={2 } z />';
|
||||
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(
|
||||
'<Component x={y} />'
|
||||
).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('calls assign with a new target object for spreads', function() {
|
||||
expectObjectAssign(
|
||||
'<Component {...x} />'
|
||||
).toBeCalledWith({}, x);
|
||||
});
|
||||
|
||||
it('calls assign with an empty object when the spread is first', function() {
|
||||
expectObjectAssign(
|
||||
'<Component { ...x } y={2} />'
|
||||
).toBeCalledWith({}, x, { y: 2 });
|
||||
});
|
||||
|
||||
it('coalesces consecutive properties into a single object', function() {
|
||||
expectObjectAssign(
|
||||
'<Component { ... x } y={2} z />'
|
||||
).toBeCalledWith({}, x, { y: 2, z: true });
|
||||
});
|
||||
|
||||
it('avoids an unnecessary empty object when spread is not first', function() {
|
||||
expectObjectAssign(
|
||||
'<Component x={1} {...y} />'
|
||||
).toBeCalledWith({x: 1}, y);
|
||||
});
|
||||
|
||||
it('passes the same value multiple times to Object.assign', function() {
|
||||
expectObjectAssign(
|
||||
'<Component x={1} y="2" {...z} {...z}><Child /></Component>'
|
||||
).toBeCalledWith({x: 1, y: "2"}, z, z);
|
||||
});
|
||||
|
||||
it('evaluates sequences before passing them to Object.assign', function() {
|
||||
expectObjectAssign(
|
||||
'<Component x="1" {...(z = { y: 2 }, z)} z={3}>Text</Component>'
|
||||
).toBeCalledWith({x: "1"}, { y: 2 }, {z: 3});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
73
vendor/fbtransform/transforms/react.js
vendored
73
vendor/fbtransform/transforms/react.js
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user