[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
Sebastian Markbage committed
Jun 25, 2014 at 12:07 UTC
e6134c307e2bb7765aaa747eb5d2136fc18abbd7
2 files changed
+140
-3
vendor/fbtransform/transforms/__tests__/react-test.js
+70
@@ -30,6 +30,26 @@ describe('react jsx', function() {
30
);
31
};
32
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
+
53
it('should convert simple tags', function() {
54
var code = [
55
'/**@jsx React.DOM*/',
@@ -357,4 +377,54 @@ describe('react jsx', function() {
377
expect(() => transform(code)).toThrow();
378
});
379
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
+
430
});
vendor/fbtransform/transforms/react.js
+70
-3
@@ -49,6 +49,14 @@ var JSX_ATTRIBUTE_TRANSFORMS = {
49
}
50
};
51
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
+
60
function visitReactTag(traverse, object, path, state) {
61
var jsxObjIdent = utils.getDocblock(state).jsx;
62
var openingElement = object.openingElement;
@@ -57,6 +65,7 @@ function visitReactTag(traverse, object, path, state) {
65
66
utils.catchup(openingElement.range[0], state, trimLeft);
67
68
+
69
if (nameObject.type === Syntax.XJSNamespacedName && nameObject.namespace) {
70
throw new Error('Namespace tags are not supported. ReactJSX is not XML.');
71
}
@@ -75,23 +84,74 @@ function visitReactTag(traverse, object, path, state) {
84
85
var hasAttributes = attributesObject.length;
86
87
+ var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) {
88
+ return attr.type === Syntax.XJSSpreadAttribute;
89
+ });
90
+
91
// 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) {
95
utils.append('{', state);
96
} else {
97
utils.append('null', state);
98
}
99
100
+ // keep track of if the previous attribute was a spread attribute
101
+ var previousWasSpread = false;
102
+
103
// write attributes
104
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
+
143
if (attr.name.namespace) {
144
throw new Error(
145
'Namespace attributes are not supported. ReactJSX is not XML.');
146
}
147
var name = attr.name.name;
92
- var isLast = index === attributesObject.length - 1;
148
149
utils.catchup(attr.range[0], state, trimLeft);
150
+
151
+ if (previousWasSpread) {
152
+ utils.append('{', state);
153
+ }
154
+
155
utils.append(quoteAttrName(name), state);
156
utils.append(': ', state);
157
@@ -119,6 +179,9 @@ function visitReactTag(traverse, object, path, state) {
179
}
180
181
utils.catchup(attr.range[1], state, trimLeft);
182
+
183
+ previousWasSpread = false;
184
+
185
});
186
187
if (!openingElement.selfClosing) {
@@ -126,10 +189,14 @@ function visitReactTag(traverse, object, path, state) {
189
utils.move(openingElement.range[1], state);
190
}
191
129
- if (hasAttributes) {
192
+ if (hasAttributes && !previousWasSpread) {
193
utils.append('}', state);
194
}
195
196
+ if (hasAtLeastOneSpreadProperty) {
197
+ utils.append(')', state);
198
+ }
199
+
200
// filter out whitespace
201
var childrenToRender = object.children.filter(function(child) {
202
return !(child.type === Syntax.Literal