[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:
Sebastian Markbage
2014-06-25 12:07:32 -07:00
committed by Paul O’Shannessy
parent 0cf686fe1e
commit e6134c307e
2 changed files with 140 additions and 3 deletions

View File

@@ -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});
});
});

View File

@@ -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