jsx whitespace codemod utility
JeffMo committed
Jan 27, 2014 at 13:07 UTC
f368f18b619d3e7182a1155deaa7e561273aec58
5 files changed
+346
bin/jsx_react_0.8_to_0.9_codemod/.gitignore
new
+1
@@ -0,0 +1 @@
1
+node_modules
bin/jsx_react_0.8_to_0.9_codemod/package.json
new
+10
@@ -0,0 +1,10 @@
1
+{
2
+ "name": "jsx_whitespace_transformer",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "esprima-fb": "~2001.1001.0-dev-harmony-fb",
6
+ "jstransform": "~2.0.1",
7
+ "node-find-files": "0.0.2",
8
+ "optimist": "~0.6.0"
9
+ }
10
+}
bin/jsx_react_0.8_to_0.9_codemod/run
new
+168
@@ -0,0 +1,168 @@
1
+#!/usr/bin/env node
2
+
3
+var esprima = require('esprima-fb');
4
+var FileFinder = require('node-find-files');
5
+var fs = require('fs');
6
+var jstransform = require('jstransform');
7
+var path = require('path');
8
+var visitReactTag = require('./transforms/react').visitReactTag;
9
+
10
+var S = esprima.Syntax;
11
+
12
+var USAGE =
13
+ 'Read a file (or directory of files) from disk, transform it so that ' +
14
+ 'React 0.9 will render it the same way as React 0.8 (whitespace-wise), and ' +
15
+ 'write the result back to disk.';
16
+
17
+function _visitFbt(node, path, state) {
18
+ return false;
19
+}
20
+_visitFbt.test = function(node, path, state) {
21
+ return node.type === S.XJSElement
22
+ && node.openingElement.name.name === 'fbt';
23
+};
24
+
25
+var VISITORS_LIST = [
26
+ _visitFbt,
27
+ visitReactTag
28
+];
29
+
30
+function _transformSource(source) {
31
+ return jstransform.transform(VISITORS_LIST, source).code;
32
+}
33
+
34
+function transformDir(dirPath, exclude) {
35
+ var finder = new FileFinder({
36
+ rootFolder: dirPath,
37
+ filterFunction: function(path, stat) {
38
+ return /\.js$/.test(path) && (!exclude || !exclude.test(path));
39
+ }
40
+ });
41
+
42
+ var numTransforms = 0;
43
+ var completeTransforms = 0;
44
+ var findingComplete = false;
45
+ function _printProgress() {
46
+ process.stdout.clearLine();
47
+ process.stdout.cursorTo(0);
48
+ process.stdout.write(
49
+ completeTransforms + '/' + numTransforms + ' transforms complete'
50
+ );
51
+
52
+ if (findingComplete && completeTransforms === numTransforms) {
53
+ console.log('\ndone!');
54
+ }
55
+ }
56
+
57
+ finder.on('match', function(pathStr, stat) {
58
+ fs.readFile(pathStr, 'utf8', function(err, data) {
59
+ if (err) {
60
+ err.message = err.message + ' (' + pathStr + ')';
61
+ throw err;
62
+ }
63
+
64
+ if (/@jsx React\.DOM/.test(data)) {
65
+ numTransforms++;
66
+ _printProgress();
67
+
68
+ var transformedData;
69
+ try {
70
+ transformedData = _transformSource(data);
71
+ } catch (e) {
72
+ e.message = e.message + ' (' + pathStr + ')';
73
+ throw e;
74
+ }
75
+
76
+ if (transformedData !== data) {
77
+ fs.writeFile(pathStr, transformedData, function(err) {
78
+ if (err) {
79
+ err.message = err.message + ' (' + pathStr + ')';
80
+ throw err;
81
+ }
82
+ completeTransforms++;
83
+ _printProgress();
84
+ });
85
+ } else {
86
+ completeTransforms++;
87
+ _printProgress();
88
+ }
89
+ }
90
+ });
91
+ });
92
+
93
+ finder.on('error', function(err) {
94
+ console.log('\nError: ', err.stack);
95
+ throw err;
96
+ });
97
+
98
+ finder.on('complete', function() {
99
+ findingComplete = true;
100
+ });
101
+
102
+ finder.startSearch();
103
+}
104
+
105
+function transformFile(pathStr) {
106
+ fs.readFile(pathStr, 'utf8', function(err, data) {
107
+ if (err) {
108
+ err.message = err.message + ' (' + pathStr + ')';
109
+ throw err;
110
+ }
111
+
112
+ if (/@jsx React\.DOM/.test(data)) {
113
+ var transformedData;
114
+ try {
115
+ transformedData = _transformSource(data);
116
+ } catch (e) {
117
+ e.message = e.message + ' (' + pathStr + ')';
118
+ throw e;
119
+ }
120
+
121
+ if (transformedData !== data) {
122
+ fs.writeFile(pathStr, transformedData, function(err) {
123
+ if (err) {
124
+ err.message = err.message + ' (' + pathStr + ')';
125
+ throw err;
126
+ }
127
+ console.log('done!');
128
+ });
129
+ } else {
130
+ console.log('done!');
131
+ }
132
+ } else {
133
+ console.error(pathStr + ' is not a JSX file!');
134
+ }
135
+ });
136
+}
137
+
138
+if (require.main === module) {
139
+ var argv = require('optimist')
140
+ .usage(USAGE)
141
+ .argv;
142
+
143
+ if (argv._.length === 0) {
144
+ throw new Error(
145
+ 'Please specify a file or directory path as the first arg!'
146
+ );
147
+ }
148
+
149
+ var absPath = path.resolve(__dirname, argv._[0]);
150
+
151
+ fs.stat(absPath, function(err, stat) {
152
+ if (err) throw err;
153
+
154
+ if (stat.isFile()) {
155
+ transformFile(absPath);
156
+ } else if (stat.isDirectory()) {
157
+ var exclude = null;
158
+ if (argv.exclude) {
159
+ exclude = new RegExp(argv.exclude);
160
+ }
161
+ transformDir(absPath, exclude);
162
+ } else {
163
+ throw new Error('Unknown filesystem node type: ' + absPath);
164
+ }
165
+ });
166
+}
167
+
168
+exports.transformDir = transformDir;
bin/jsx_react_0.8_to_0.9_codemod/transforms/react.js
new
+64
@@ -0,0 +1,64 @@
1
+/**
2
+ * Copyright 2013 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+/*global exports:true*/
17
+"use strict";
18
+var Syntax = require('esprima-fb').Syntax;
19
+var utils = require('jstransform/src/utils');
20
+
21
+var renderXJSExpressionContainer =
22
+ require('./xjs').renderXJSExpressionContainer;
23
+var renderXJSLiteral = require('./xjs').renderXJSLiteral;
24
+
25
+function visitReactTag(traverse, object, path, state) {
26
+ if (object.name.namespace) {
27
+ throw new Error(
28
+ 'Namespace tags are not supported. ReactJSX is not XML.');
29
+ }
30
+
31
+ object.children.forEach(function(child, ii) {
32
+ if (child.type === Syntax.Literal) {
33
+ renderXJSLiteral(child, state);
34
+ } else if (child.type === Syntax.XJSExpressionContainer) {
35
+
36
+ var isNotAfterLiteral =
37
+ ii == 0 ||
38
+ object.children[ii - 1].type !== Syntax.Literal;
39
+
40
+ var isNotBeforeLiteral =
41
+ ii == object.children.length - 1 ||
42
+ object.children[ii + 1].type !== Syntax.Literal;
43
+
44
+ renderXJSExpressionContainer(
45
+ traverse, child,
46
+ isNotAfterLiteral,
47
+ isNotBeforeLiteral,
48
+ path, state);
49
+
50
+ } else {
51
+ traverse(child, path, state);
52
+ }
53
+ });
54
+
55
+ return false;
56
+}
57
+
58
+visitReactTag.test = function(object, path, state) {
59
+ // only run react when react @jsx namespace is specified in docblock
60
+ var jsx = utils.getDocblock(state).jsx;
61
+ return object.type === Syntax.XJSElement && jsx && jsx.length;
62
+};
63
+
64
+exports.visitReactTag = visitReactTag;
bin/jsx_react_0.8_to_0.9_codemod/transforms/xjs.js
new
+103
@@ -0,0 +1,103 @@
1
+/**
2
+ * Copyright 2013 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+/*global exports:true*/
17
+"use strict";
18
+var Syntax = require('esprima-fb').Syntax;
19
+var utils = require('jstransform/src/utils');
20
+
21
+function renderXJSLiteral(object, state) {
22
+ var value = object.raw;
23
+
24
+ utils.catchup(object.range[0], state);
25
+
26
+ /*
27
+ This can be used to "annotate spaces" inserted by this transformation,
28
+ so that they can be more easily recognized as such in the final code
29
+
30
+ {' '}
31
+ {'\\x20'}
32
+ {(' ')}
33
+ {' '||0}
34
+ {' '||AnyTextYouLike}
35
+ {' '||'AnyTextYouLike'}
36
+ {GlobalVariableWithASpace}
37
+ */
38
+
39
+ var space = "{' '}";
40
+
41
+ /*
42
+ · space
43
+ ¬ newline
44
+ {expr} = <tag>
45
+
46
+ Old whitespace rules:
47
+ {1}··Aaa··Bbb··{2}··{3} → {1}·Aaa··Bbb·{2}{3}
48
+ {1}¬¬Aaa¬¬Bbb¬¬{2}¬¬{3} → {1}·Aaa·Bbb·{2}{3}
49
+
50
+ New whitespace rules:
51
+ {1}··Aaa··Bbb··{2}··{3} → {1}··Aaa··Bbb··{2}··{3}
52
+ {1}¬¬Aaa¬¬Bbb¬¬{2}¬¬{3} → {1}Aaa·Bbb{2}{3}
53
+
54
+ Required transformation:
55
+ {1}··{2} = {1}··{2} → {1}{2}
56
+ {1}··Aaa··{2} = {1}··Aaa··{2} → {1}·Aaa·{2}
57
+ {1}¬¬Aaa¬¬{2} = {1}Aaa{2} → {1}·Aaa·{2}
58
+ */
59
+
60
+ // {1}··{2} = {1}··{2} → {1}{2}
61
+ value = value.replace(/^[ \t]+$/, '');
62
+
63
+ // {1}··Aaa··{2} = {1}··Aaa··{2} → {1}·Aaa·{2}
64
+ value = value.replace(/^[ \t]+([^ \t\r\n])/, " $1");
65
+ value = value.replace(/([^ \t\r\n])[ \t]+$/, "$1 ");
66
+
67
+ // {1}¬¬Aaa¬¬{2} = {1}Aaa{2} → {1}·Aaa·{2}
68
+ value = value.replace(/^([ \t]*[\r\n][ \t\r\n]*)([^ \t\r\n].*)/, "$1" + space + "$2");
69
+ value = value.replace(/([^ \t\r\n])([ \t]*[\r\n][ \t\r\n]*)$/, "$1" + space + "$2");
70
+
71
+ // Rendered whitespace tabs are replaced with spaces
72
+ value = value.replace(/[^ \t\r\n][ ]*[\t][ \t]*[^ \t\r\n]/, function(match) {
73
+ return match.replace(/\t/g, ' ');
74
+ });
75
+
76
+ utils.append(value, state);
77
+ utils.move(object.range[1], state);
78
+}
79
+
80
+function renderXJSExpressionContainer(
81
+ traverse, object,
82
+ isNotAfterLiteral,
83
+ isNotBeforeLiteral,
84
+ path, state)
85
+{
86
+ utils.catchup(object.range[0], state);
87
+
88
+ // Unbox the previously required {' '}-workaround
89
+ var raw = object.expression.raw;
90
+ var isSpace = (raw === "' '" || raw === '" "');
91
+
92
+ if (isNotAfterLiteral && isNotBeforeLiteral && isSpace) {
93
+ utils.append(' ', state);
94
+ utils.move(object.range[1], state);
95
+ } else {
96
+ utils.catchup(object.range[1], state);
97
+ }
98
+
99
+ return false;
100
+}
101
+
102
+exports.renderXJSExpressionContainer = renderXJSExpressionContainer;
103
+exports.renderXJSLiteral = renderXJSLiteral;