Skip to content

Commit e6134c3

Browse files
sebmarkbagezpao
authored andcommitted
[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 facebookarchive/esprima#22
1 parent 0cf686f commit e6134c3

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

vendor/fbtransform/transforms/__tests__/react-test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ describe('react jsx', function() {
3030
);
3131
};
3232

33+
// These are placeholder variables in scope that we can use to assert that a
34+
// specific variable reference was passed, rather than an object clone of it.
35+
var x = 123456;
36+
var y = 789012;
37+
var z = 345678;
38+
39+
var HEADER =
40+
'/**\n' +
41+
' * @jsx React.DOM\n' +
42+
' */\n';
43+
44+
var expectObjectAssign = function(code) {
45+
var Component = jest.genMockFunction();
46+
var Child = jest.genMockFunction();
47+
var objectAssignMock = jest.genMockFunction();
48+
Object.assign = objectAssignMock;
49+
eval(transform(HEADER + code).code);
50+
return expect(objectAssignMock);
51+
}
52+
3353
it('should convert simple tags', function() {
3454
var code = [
3555
'/**@jsx React.DOM*/',
@@ -357,4 +377,54 @@ describe('react jsx', function() {
357377
expect(() => transform(code)).toThrow();
358378
});
359379

380+
it('wraps props in Object.assign for spread attributes', function() {
381+
var code = HEADER +
382+
'<Component { ... x } y\n={2 } z />';
383+
var result = HEADER +
384+
'Component(Object.assign({}, x , {y: \n2, z: true}))';
385+
expect(transform(code).code).toBe(result);
386+
});
387+
388+
it('does not call Object.assign when there are no spreads', function() {
389+
expectObjectAssign(
390+
'<Component x={y} />'
391+
).not.toBeCalled();
392+
});
393+
394+
it('calls assign with a new target object for spreads', function() {
395+
expectObjectAssign(
396+
'<Component {...x} />'
397+
).toBeCalledWith({}, x);
398+
});
399+
400+
it('calls assign with an empty object when the spread is first', function() {
401+
expectObjectAssign(
402+
'<Component { ...x } y={2} />'
403+
).toBeCalledWith({}, x, { y: 2 });
404+
});
405+
406+
it('coalesces consecutive properties into a single object', function() {
407+
expectObjectAssign(
408+
'<Component { ... x } y={2} z />'
409+
).toBeCalledWith({}, x, { y: 2, z: true });
410+
});
411+
412+
it('avoids an unnecessary empty object when spread is not first', function() {
413+
expectObjectAssign(
414+
'<Component x={1} {...y} />'
415+
).toBeCalledWith({x: 1}, y);
416+
});
417+
418+
it('passes the same value multiple times to Object.assign', function() {
419+
expectObjectAssign(
420+
'<Component x={1} y="2" {...z} {...z}><Child /></Component>'
421+
).toBeCalledWith({x: 1, y: "2"}, z, z);
422+
});
423+
424+
it('evaluates sequences before passing them to Object.assign', function() {
425+
expectObjectAssign(
426+
'<Component x="1" {...(z = { y: 2 }, z)} z={3}>Text</Component>'
427+
).toBeCalledWith({x: "1"}, { y: 2 }, {z: 3});
428+
});
429+
360430
});

vendor/fbtransform/transforms/react.js

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ var JSX_ATTRIBUTE_TRANSFORMS = {
4949
}
5050
};
5151

52+
/**
53+
* Removes all non-whitespace/parenthesis characters
54+
*/
55+
var reNonWhiteParen = /([^\s\(\)])/g;
56+
function stripNonWhiteParen(value) {
57+
return value.replace(reNonWhiteParen, '');
58+
}
59+
5260
function visitReactTag(traverse, object, path, state) {
5361
var jsxObjIdent = utils.getDocblock(state).jsx;
5462
var openingElement = object.openingElement;
@@ -57,6 +65,7 @@ function visitReactTag(traverse, object, path, state) {
5765

5866
utils.catchup(openingElement.range[0], state, trimLeft);
5967

68+
6069
if (nameObject.type === Syntax.XJSNamespacedName && nameObject.namespace) {
6170
throw new Error('Namespace tags are not supported. ReactJSX is not XML.');
6271
}
@@ -75,23 +84,74 @@ function visitReactTag(traverse, object, path, state) {
7584

7685
var hasAttributes = attributesObject.length;
7786

87+
var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) {
88+
return attr.type === Syntax.XJSSpreadAttribute;
89+
});
90+
7891
// if we don't have any attributes, pass in null
79-
if (hasAttributes) {
92+
if (hasAtLeastOneSpreadProperty) {
93+
utils.append('Object.assign({', state);
94+
} else if (hasAttributes) {
8095
utils.append('{', state);
8196
} else {
8297
utils.append('null', state);
8398
}
8499

100+
// keep track of if the previous attribute was a spread attribute
101+
var previousWasSpread = false;
102+
85103
// write attributes
86104
attributesObject.forEach(function(attr, index) {
105+
var isLast = index === attributesObject.length - 1;
106+
107+
if (attr.type === Syntax.XJSSpreadAttribute) {
108+
// Plus 1 to skip `{`.
109+
utils.move(attr.range[0] + 1, state);
110+
111+
// Close the previous object or initial object
112+
if (!previousWasSpread) {
113+
utils.append('}, ', state);
114+
}
115+
116+
// Move to the expression start, ignoring everything except parenthesis
117+
// and whitespace.
118+
utils.catchup(attr.argument.range[0], state, stripNonWhiteParen);
119+
120+
traverse(attr.argument, path, state);
121+
122+
utils.catchup(attr.argument.range[1], state);
123+
124+
// Move to the end, ignoring parenthesis and the closing `}`
125+
utils.catchup(attr.range[1] - 1, state, stripNonWhiteParen);
126+
127+
if (!isLast) {
128+
utils.append(', ', state);
129+
}
130+
131+
utils.move(attr.range[1], state);
132+
133+
previousWasSpread = true;
134+
135+
return;
136+
}
137+
138+
// If the next attribute is a spread, we're effective last in this object
139+
if (!isLast) {
140+
isLast = attributesObject[index + 1].type === Syntax.XJSSpreadAttribute;
141+
}
142+
87143
if (attr.name.namespace) {
88144
throw new Error(
89145
'Namespace attributes are not supported. ReactJSX is not XML.');
90146
}
91147
var name = attr.name.name;
92-
var isLast = index === attributesObject.length - 1;
93148

94149
utils.catchup(attr.range[0], state, trimLeft);
150+
151+
if (previousWasSpread) {
152+
utils.append('{', state);
153+
}
154+
95155
utils.append(quoteAttrName(name), state);
96156
utils.append(': ', state);
97157

@@ -119,17 +179,24 @@ function visitReactTag(traverse, object, path, state) {
119179
}
120180

121181
utils.catchup(attr.range[1], state, trimLeft);
182+
183+
previousWasSpread = false;
184+
122185
});
123186

124187
if (!openingElement.selfClosing) {
125188
utils.catchup(openingElement.range[1] - 1, state, trimLeft);
126189
utils.move(openingElement.range[1], state);
127190
}
128191

129-
if (hasAttributes) {
192+
if (hasAttributes && !previousWasSpread) {
130193
utils.append('}', state);
131194
}
132195

196+
if (hasAtLeastOneSpreadProperty) {
197+
utils.append(')', state);
198+
}
199+
133200
// filter out whitespace
134201
var childrenToRender = object.children.filter(function(child) {
135202
return !(child.type === Syntax.Literal

0 commit comments

Comments
 (0)