Add react-to-react-dom codemod
I ran it over Facebook so it probably works.
Ben Alpert committed
Sep 1, 2015 at 17:36 UTC
fb7e566b1294f919e02bc27272d741b0ebe12047
2 files changed
+330
-1
packages/react-codemod/README.md
+10
-1
@@ -45,10 +45,19 @@ are using the master version (>0.13.1) of React as it is using
45
if you are using or planning to use [Flow](http://flowtype.org/). Also make
46
sure you are not calling `setState` anywhere outside of your component.
47
48
-All scripts take an option `--no-explicit-require=true` if you don't have a
48
+These three scripts take an option `--no-explicit-require=true` if you don't have a
49
`require('React')` statement in your code files and if you access React as a
50
global.
51
52
+`react-to-react-dom` updates code for the split of the `react` and `react-dom`
53
+packages (e.g., `React.render` to `ReactDOM.render`). It looks for
54
+`require('react')` and replaces the appropriate property accesses using
55
+`require('react-dom')`. It does not support ES6 modules or other non-CommonJS
56
+systems.
57
+
58
+ * `jscodeshift -t react/packages/react-codemod/transforms/react-to-react-dom.js <file>`
59
+ * After running the automated codemod, you may want to run a regex-based find-and-replace to remove extra whitespace between the added requires, such as `codemod.py -m -d src --extensions js '(var React\s*=\s*require\(.react.\);)\n\n(\s*var ReactDOM)' '\1\n\2'` using https://github.com/facebook/codemod.
60
+
61
### Explanation of the ES2015 class transform
62
63
* Ignore components with calls to deprecated APIs. This is very defensive, if
packages/react-codemod/transforms/react-to-react-dom.js
new
+320
@@ -0,0 +1,320 @@
1
+/**
2
+ * Copyright 2013-2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ */
10
+
11
+'use strict';
12
+
13
+var CORE_PROPERTIES = [
14
+ 'Children',
15
+ 'Component',
16
+ 'createElement',
17
+ 'cloneElement',
18
+ 'isValidElement',
19
+ 'PropTypes',
20
+ 'createClass',
21
+ 'createFactory',
22
+ 'createMixin',
23
+ 'DOM',
24
+ '__spread',
25
+];
26
+
27
+var DOM_PROPERTIES = [
28
+ 'findDOMNode',
29
+ 'render',
30
+ 'unmountComponentAtNode',
31
+ 'unstable_batchedUpdates',
32
+ 'unstable_renderSubtreeIntoContainer',
33
+];
34
+
35
+var DOM_SERVER_PROPERTIES = [
36
+ 'renderToString',
37
+ 'renderToStaticMarkup',
38
+];
39
+
40
+function reportError(node, error) {
41
+ throw new Error(
42
+ `At ${node.loc.start.line}:${node.loc.start.column}: ${error}`
43
+ );
44
+}
45
+
46
+function isRequire(path, moduleName) {
47
+ return (
48
+ path.value.type === 'CallExpression' &&
49
+ path.value.callee.type === 'Identifier' &&
50
+ path.value.callee.name === 'require' &&
51
+ path.value.arguments.length === 1 &&
52
+ path.value.arguments[0].type === 'Literal' &&
53
+ path.value.arguments[0].value === moduleName
54
+ );
55
+}
56
+
57
+module.exports = function(file, api) {
58
+ var j = api.jscodeshift;
59
+ var root = j(file.source);
60
+
61
+ [
62
+ ['React', 'ReactDOM', 'ReactDOMServer'],
63
+ ['react', 'react-dom', 'react-dom/server'],
64
+ ].forEach(function(pair) {
65
+ var coreModuleName = pair[0];
66
+ var domModuleName = pair[1];
67
+ var domServerModuleName = pair[2];
68
+
69
+ var domAlreadyDeclared = false;
70
+ var domServerAlreadyDeclared = false;
71
+
72
+ var coreRequireDeclarator;
73
+ root
74
+ .find(j.CallExpression)
75
+ .filter(p => isRequire(p, coreModuleName))
76
+ .forEach(p => {
77
+ if (p.parent.value.type === 'VariableDeclarator') {
78
+ if (p.parent.value.id.type === 'ObjectPattern') {
79
+ var pattern = p.parent.value.id;
80
+ var all = pattern.properties.every(function(prop) {
81
+ if (prop.key.type === 'Identifier') {
82
+ var name = prop.key.name;
83
+ return CORE_PROPERTIES.indexOf(name) !== -1;
84
+ }
85
+ return false;
86
+ });
87
+ if (all) {
88
+ // var {PropTypes} = require('React'); so leave alone
89
+ return;
90
+ }
91
+ }
92
+ if (coreRequireDeclarator) {
93
+ reportError(
94
+ p.value,
95
+ 'Multiple declarations of React'
96
+ );
97
+ }
98
+ if (p.parent.value.id.type !== 'Identifier') {
99
+ reportError(
100
+ p.value,
101
+ 'Unexpected destructuring in require of ' + coreModuleName
102
+ );
103
+ }
104
+ var name = p.parent.value.id.name;
105
+ var scope = p.scope.lookup(name);
106
+ if (scope.declares('ReactDOM')) {
107
+ console.log('Using existing ReactDOM var in ' + file.path);
108
+ domAlreadyDeclared = true;
109
+ }
110
+ if (scope.declares('ReactDOMServer')) {
111
+ console.log('Using existing ReactDOMServer var in ' + file.path);
112
+ domServerAlreadyDeclared = true;
113
+ }
114
+ coreRequireDeclarator = p.parent;
115
+ } else if (p.parent.value.type === 'AssignmentExpression') {
116
+ if (p.parent.value.left.type !== 'Identifier') {
117
+ reportError(
118
+ p.value,
119
+ 'Unexpected destructuring in require of ' + coreModuleName
120
+ );
121
+ }
122
+ var name = p.parent.value.left.name;
123
+ var scope = p.scope.lookup(name);
124
+ var reactBindings = scope.getBindings()[name];
125
+ if (reactBindings.length !== 1) {
126
+ throw new Error(
127
+ 'Unexpected number of bindings: ' + reactBindings.length
128
+ );
129
+ }
130
+ coreRequireDeclarator = reactBindings[0].parent;
131
+ if (coreRequireDeclarator.value.init &&
132
+ !isRequire(coreRequireDeclarator.get('init'), coreModuleName)) {
133
+ reportError(
134
+ coreRequireDeclarator.value,
135
+ 'Unexpected initialization of ' + coreModuleName
136
+ );
137
+ }
138
+ if (scope.declares('ReactDOM')) {
139
+ console.log('Using existing ReactDOM var in ' + file.path);
140
+ domAlreadyDeclared = true;
141
+ }
142
+ if (scope.declares('ReactDOMServer')) {
143
+ console.log('Using existing ReactDOMServer var in ' + file.path);
144
+ domServerAlreadyDeclared = true;
145
+ }
146
+ }
147
+ });
148
+ if (!coreRequireDeclarator) {
149
+ return;
150
+ }
151
+
152
+ if (!domAlreadyDeclared &&
153
+ root.find(j.Identifier, {name: 'ReactDOM'}).size() > 0) {
154
+ throw new Error(
155
+ 'ReactDOM is already defined in a different scope than React'
156
+ );
157
+ }
158
+ if (!domServerAlreadyDeclared &&
159
+ root.find(j.Identifier, {name: 'ReactDOMServer'}).size() > 0) {
160
+ throw new Error(
161
+ 'ReactDOMServer is already defined in a different scope than React'
162
+ );
163
+ }
164
+
165
+ var coreName = coreRequireDeclarator.value.id.name;
166
+
167
+ var processed = new Set();
168
+ var requireAssignments = [];
169
+ var coreUses = 0;
170
+ var domUses = 0;
171
+ var domServerUses = 0;
172
+
173
+ root
174
+ .find(j.Identifier, {name: coreName})
175
+ .forEach(p => {
176
+ if (processed.has(p.value)) {
177
+ // https://github.com/facebook/jscodeshift/issues/36
178
+ return;
179
+ }
180
+ processed.add(p.value);
181
+ if (p.parent.value.type === 'MemberExpression' ||
182
+ p.parent.value.type === 'QualifiedTypeIdentifier') {
183
+ var left;
184
+ var right;
185
+ if (p.parent.value.type === 'MemberExpression') {
186
+ left = p.parent.value.object;
187
+ right = p.parent.value.property;
188
+ } else {
189
+ left = p.parent.value.qualification;
190
+ right = p.parent.value.id;
191
+ }
192
+ if (left === p.value) {
193
+ // React.foo (or React[foo])
194
+ if (right.type === 'Identifier') {
195
+ var name = right.name;
196
+ if (CORE_PROPERTIES.indexOf(name) !== -1) {
197
+ coreUses++;
198
+ } else if (DOM_PROPERTIES.indexOf(name) !== -1) {
199
+ domUses++;
200
+ j(p).replaceWith(j.identifier('ReactDOM'));
201
+ } else if (DOM_SERVER_PROPERTIES.indexOf(name) !== -1) {
202
+ domServerUses++;
203
+ j(p).replaceWith(j.identifier('ReactDOMServer'));
204
+ } else {
205
+ throw new Error('Unknown property React.' + name);
206
+ }
207
+ }
208
+ } else if (right === p.value) {
209
+ // foo.React, no need to transform
210
+ } else {
211
+ throw new Error('unimplemented');
212
+ }
213
+ } else if (p.parent.value.type === 'VariableDeclarator') {
214
+ if (p.parent.value.id === p.value) {
215
+ // var React = ...;
216
+ } else if (p.parent.value.init === p.value) {
217
+ // var ... = React;
218
+ var pattern = p.parent.value.id;
219
+ if (pattern.type === 'ObjectPattern') {
220
+ // var {PropTypes} = React;
221
+ // Most of these cases will just be looking at {PropTypes} so this
222
+ // is usually a no-op.
223
+ var coreProperties = [];
224
+ var domProperties = [];
225
+ pattern.properties.forEach(function(prop) {
226
+ if (prop.key.type === 'Identifier') {
227
+ var key = prop.key.name;
228
+ if (CORE_PROPERTIES.indexOf(key) !== -1) {
229
+ coreProperties.push(prop);
230
+ } else if (DOM_PROPERTIES.indexOf(key) !== -1) {
231
+ domProperties.push(prop);
232
+ } else {
233
+ throw new Error(
234
+ 'Unknown property React.' + key + ' while destructuring'
235
+ );
236
+ }
237
+ } else {
238
+ throw new Error('unimplemented');
239
+ }
240
+ });
241
+ var domDeclarator = j.variableDeclarator(
242
+ j.objectPattern(domProperties),
243
+ j.identifier('ReactDOM')
244
+ );
245
+ if (coreProperties.length && !domProperties.length) {
246
+ // nothing to do
247
+ coreUses++;
248
+ } else if (domProperties.length && !coreProperties.length) {
249
+ domUses++;
250
+ j(p.parent).replaceWith(domDeclarator);
251
+ } else {
252
+ coreUses++;
253
+ domUses++;
254
+ var decl = j(p).closest(j.VariableDeclaration);
255
+ decl.insertAfter(j.variableDeclaration(
256
+ decl.get().value.kind,
257
+ [domDeclarator]
258
+ ));
259
+ }
260
+ } else {
261
+ throw new Error('unimplemented');
262
+ }
263
+ } else {
264
+ throw new Error('unimplemented');
265
+ }
266
+ } else if (p.parent.value.type === 'AssignmentExpression') {
267
+ if (p.parent.value.left === p.value) {
268
+ if (isRequire(p.parent.get('right'), coreModuleName)) {
269
+ requireAssignments.push(p.parent);
270
+ } else {
271
+ reportError(
272
+ p.parent.value,
273
+ 'Unexpected assignment to ' + coreModuleName
274
+ );
275
+ }
276
+ } else {
277
+ throw new Error('unimplemented');
278
+ }
279
+ } else {
280
+ reportError(p.value, 'unimplemented ' + p.parent.value.type);
281
+ }
282
+ });
283
+
284
+ coreUses += root.find(j.JSXElement).size();
285
+
286
+ function insertRequire(name, path) {
287
+ var req = j.callExpression(
288
+ j.identifier('require'),
289
+ [j.literal(path)]
290
+ );
291
+ requireAssignments.forEach(function(requireAssignment) {
292
+ requireAssignment.parent.insertAfter(
293
+ j.expressionStatement(
294
+ j.assignmentExpression('=', j.identifier(name), req)
295
+ )
296
+ );
297
+ });
298
+ coreRequireDeclarator.parent.insertAfter(j.variableDeclaration(
299
+ coreRequireDeclarator.parent.value.kind,
300
+ [j.variableDeclarator(
301
+ j.identifier(name),
302
+ coreRequireDeclarator.value.init ? req : null
303
+ )]
304
+ ));
305
+ }
306
+
307
+ if (domServerUses > 0 && !domServerAlreadyDeclared) {
308
+ insertRequire('ReactDOMServer', domServerModuleName);
309
+ }
310
+ if (domUses > 0 && !domAlreadyDeclared) {
311
+ insertRequire('ReactDOM', domModuleName);
312
+ }
313
+ if ((domUses > 0 || domServerUses > 0) && coreUses === 0) {
314
+ j(coreRequireDeclarator).remove();
315
+ requireAssignments.forEach(r => j(r).remove());
316
+ }
317
+ });
318
+
319
+ return root.toSource({quote: 'single'});
320
+};