@samitouri / QOS-React-2 / commits / e6134c307e

[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