diff --git a/bin/jsx_react_0.8_to_0.9_codemod/.gitignore b/bin/jsx_react_0.8_to_0.9_codemod/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/bin/jsx_react_0.8_to_0.9_codemod/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/bin/jsx_react_0.8_to_0.9_codemod/package.json b/bin/jsx_react_0.8_to_0.9_codemod/package.json new file mode 100644 index 0000000000..fff7f3c59f --- /dev/null +++ b/bin/jsx_react_0.8_to_0.9_codemod/package.json @@ -0,0 +1,10 @@ +{ + "name": "jsx_whitespace_transformer", + "version": "1.0.0", + "dependencies": { + "esprima-fb": "~2001.1001.0-dev-harmony-fb", + "jstransform": "~2.0.1", + "node-find-files": "0.0.2", + "optimist": "~0.6.0" + } +} diff --git a/bin/jsx_react_0.8_to_0.9_codemod/run b/bin/jsx_react_0.8_to_0.9_codemod/run new file mode 100755 index 0000000000..246af42b54 --- /dev/null +++ b/bin/jsx_react_0.8_to_0.9_codemod/run @@ -0,0 +1,168 @@ +#!/usr/bin/env node + +var esprima = require('esprima-fb'); +var FileFinder = require('node-find-files'); +var fs = require('fs'); +var jstransform = require('jstransform'); +var path = require('path'); +var visitReactTag = require('./transforms/react').visitReactTag; + +var S = esprima.Syntax; + +var USAGE = + 'Read a file (or directory of files) from disk, transform it so that ' + + 'React 0.9 will render it the same way as React 0.8 (whitespace-wise), and ' + + 'write the result back to disk.'; + +function _visitFbt(node, path, state) { + return false; +} +_visitFbt.test = function(node, path, state) { + return node.type === S.XJSElement + && node.openingElement.name.name === 'fbt'; +}; + +var VISITORS_LIST = [ + _visitFbt, + visitReactTag +]; + +function _transformSource(source) { + return jstransform.transform(VISITORS_LIST, source).code; +} + +function transformDir(dirPath, exclude) { + var finder = new FileFinder({ + rootFolder: dirPath, + filterFunction: function(path, stat) { + return /\.js$/.test(path) && (!exclude || !exclude.test(path)); + } + }); + + var numTransforms = 0; + var completeTransforms = 0; + var findingComplete = false; + function _printProgress() { + process.stdout.clearLine(); + process.stdout.cursorTo(0); + process.stdout.write( + completeTransforms + '/' + numTransforms + ' transforms complete' + ); + + if (findingComplete && completeTransforms === numTransforms) { + console.log('\ndone!'); + } + } + + finder.on('match', function(pathStr, stat) { + fs.readFile(pathStr, 'utf8', function(err, data) { + if (err) { + err.message = err.message + ' (' + pathStr + ')'; + throw err; + } + + if (/@jsx React\.DOM/.test(data)) { + numTransforms++; + _printProgress(); + + var transformedData; + try { + transformedData = _transformSource(data); + } catch (e) { + e.message = e.message + ' (' + pathStr + ')'; + throw e; + } + + if (transformedData !== data) { + fs.writeFile(pathStr, transformedData, function(err) { + if (err) { + err.message = err.message + ' (' + pathStr + ')'; + throw err; + } + completeTransforms++; + _printProgress(); + }); + } else { + completeTransforms++; + _printProgress(); + } + } + }); + }); + + finder.on('error', function(err) { + console.log('\nError: ', err.stack); + throw err; + }); + + finder.on('complete', function() { + findingComplete = true; + }); + + finder.startSearch(); +} + +function transformFile(pathStr) { + fs.readFile(pathStr, 'utf8', function(err, data) { + if (err) { + err.message = err.message + ' (' + pathStr + ')'; + throw err; + } + + if (/@jsx React\.DOM/.test(data)) { + var transformedData; + try { + transformedData = _transformSource(data); + } catch (e) { + e.message = e.message + ' (' + pathStr + ')'; + throw e; + } + + if (transformedData !== data) { + fs.writeFile(pathStr, transformedData, function(err) { + if (err) { + err.message = err.message + ' (' + pathStr + ')'; + throw err; + } + console.log('done!'); + }); + } else { + console.log('done!'); + } + } else { + console.error(pathStr + ' is not a JSX file!'); + } + }); +} + +if (require.main === module) { + var argv = require('optimist') + .usage(USAGE) + .argv; + + if (argv._.length === 0) { + throw new Error( + 'Please specify a file or directory path as the first arg!' + ); + } + + var absPath = path.resolve(__dirname, argv._[0]); + + fs.stat(absPath, function(err, stat) { + if (err) throw err; + + if (stat.isFile()) { + transformFile(absPath); + } else if (stat.isDirectory()) { + var exclude = null; + if (argv.exclude) { + exclude = new RegExp(argv.exclude); + } + transformDir(absPath, exclude); + } else { + throw new Error('Unknown filesystem node type: ' + absPath); + } + }); +} + +exports.transformDir = transformDir; diff --git a/bin/jsx_react_0.8_to_0.9_codemod/transforms/react.js b/bin/jsx_react_0.8_to_0.9_codemod/transforms/react.js new file mode 100644 index 0000000000..b0814d3da7 --- /dev/null +++ b/bin/jsx_react_0.8_to_0.9_codemod/transforms/react.js @@ -0,0 +1,64 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*global exports:true*/ +"use strict"; +var Syntax = require('esprima-fb').Syntax; +var utils = require('jstransform/src/utils'); + +var renderXJSExpressionContainer = + require('./xjs').renderXJSExpressionContainer; +var renderXJSLiteral = require('./xjs').renderXJSLiteral; + +function visitReactTag(traverse, object, path, state) { + if (object.name.namespace) { + throw new Error( + 'Namespace tags are not supported. ReactJSX is not XML.'); + } + + object.children.forEach(function(child, ii) { + if (child.type === Syntax.Literal) { + renderXJSLiteral(child, state); + } else if (child.type === Syntax.XJSExpressionContainer) { + + var isNotAfterLiteral = + ii == 0 || + object.children[ii - 1].type !== Syntax.Literal; + + var isNotBeforeLiteral = + ii == object.children.length - 1 || + object.children[ii + 1].type !== Syntax.Literal; + + renderXJSExpressionContainer( + traverse, child, + isNotAfterLiteral, + isNotBeforeLiteral, + path, state); + + } else { + traverse(child, path, state); + } + }); + + return false; +} + +visitReactTag.test = function(object, path, state) { + // only run react when react @jsx namespace is specified in docblock + var jsx = utils.getDocblock(state).jsx; + return object.type === Syntax.XJSElement && jsx && jsx.length; +}; + +exports.visitReactTag = visitReactTag; diff --git a/bin/jsx_react_0.8_to_0.9_codemod/transforms/xjs.js b/bin/jsx_react_0.8_to_0.9_codemod/transforms/xjs.js new file mode 100644 index 0000000000..e118ad8281 --- /dev/null +++ b/bin/jsx_react_0.8_to_0.9_codemod/transforms/xjs.js @@ -0,0 +1,103 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*global exports:true*/ +"use strict"; +var Syntax = require('esprima-fb').Syntax; +var utils = require('jstransform/src/utils'); + +function renderXJSLiteral(object, state) { + var value = object.raw; + + utils.catchup(object.range[0], state); + + /* + This can be used to "annotate spaces" inserted by this transformation, + so that they can be more easily recognized as such in the final code + + {' '} + {'\\x20'} + {(' ')} + {' '||0} + {' '||AnyTextYouLike} + {' '||'AnyTextYouLike'} + {GlobalVariableWithASpace} + */ + + var space = "{' '}"; + + /* + · space + ¬ newline + {expr} = + + Old whitespace rules: + {1}··Aaa··Bbb··{2}··{3} → {1}·Aaa··Bbb·{2}{3} + {1}¬¬Aaa¬¬Bbb¬¬{2}¬¬{3} → {1}·Aaa·Bbb·{2}{3} + + New whitespace rules: + {1}··Aaa··Bbb··{2}··{3} → {1}··Aaa··Bbb··{2}··{3} + {1}¬¬Aaa¬¬Bbb¬¬{2}¬¬{3} → {1}Aaa·Bbb{2}{3} + + Required transformation: + {1}··{2} = {1}··{2} → {1}{2} + {1}··Aaa··{2} = {1}··Aaa··{2} → {1}·Aaa·{2} + {1}¬¬Aaa¬¬{2} = {1}Aaa{2} → {1}·Aaa·{2} + */ + + // {1}··{2} = {1}··{2} → {1}{2} + value = value.replace(/^[ \t]+$/, ''); + + // {1}··Aaa··{2} = {1}··Aaa··{2} → {1}·Aaa·{2} + value = value.replace(/^[ \t]+([^ \t\r\n])/, " $1"); + value = value.replace(/([^ \t\r\n])[ \t]+$/, "$1 "); + + // {1}¬¬Aaa¬¬{2} = {1}Aaa{2} → {1}·Aaa·{2} + value = value.replace(/^([ \t]*[\r\n][ \t\r\n]*)([^ \t\r\n].*)/, "$1" + space + "$2"); + value = value.replace(/([^ \t\r\n])([ \t]*[\r\n][ \t\r\n]*)$/, "$1" + space + "$2"); + + // Rendered whitespace tabs are replaced with spaces + value = value.replace(/[^ \t\r\n][ ]*[\t][ \t]*[^ \t\r\n]/, function(match) { + return match.replace(/\t/g, ' '); + }); + + utils.append(value, state); + utils.move(object.range[1], state); +} + +function renderXJSExpressionContainer( + traverse, object, + isNotAfterLiteral, + isNotBeforeLiteral, + path, state) +{ + utils.catchup(object.range[0], state); + + // Unbox the previously required {' '}-workaround + var raw = object.expression.raw; + var isSpace = (raw === "' '" || raw === '" "'); + + if (isNotAfterLiteral && isNotBeforeLiteral && isSpace) { + utils.append(' ', state); + utils.move(object.range[1], state); + } else { + utils.catchup(object.range[1], state); + } + + return false; +} + +exports.renderXJSExpressionContainer = renderXJSExpressionContainer; +exports.renderXJSLiteral = renderXJSLiteral;