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

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;