Last active
March 15, 2016 09:17
-
-
Save zhdanovme/67d600822edae510f1ab to your computer and use it in GitHub Desktop.
recast-xhh.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // Запускать с уровня где лежит папка hh.sites.main | |
| // Usage: ./recast-xhh.js HH/Foo | |
| 'use strict'; | |
| var recast = require('recast'); | |
| var glob = require('glob'); | |
| var fs = require('fs'); | |
| var cp = require('child_process'); | |
| var exec = require('child_process').exec; | |
| var builder = recast.types.builders; | |
| var STATIC_JS_PATH = './hh.sites.main/webapp-static/js/'; | |
| var processFile = process.argv[2] + '.js'; | |
| if (processFile) { | |
| console.log('Processing only ' + processFile); | |
| } else { | |
| return; | |
| } | |
| var RESERVED_WORDS = [ | |
| 'Array', | |
| 'Date', | |
| 'Infinity', | |
| 'Math', | |
| 'NaN', | |
| 'Number', | |
| 'String', | |
| 'Object' | |
| ]; | |
| var files = glob.sync(STATIC_JS_PATH + '@(jsx|HHC)/*.js'); | |
| var depModules = files.map(function(filepath) { | |
| return filepath.replace(STATIC_JS_PATH, '').replace('.js', ''); | |
| }); | |
| var depModuleVariables = {}; | |
| for (var i in depModules) { | |
| var depModule = depModules[i]; | |
| depModuleVariables[depModule] = depModule.replace('jsx', '').replace('HHC', '').split('/').join(''); | |
| } | |
| var depModuleLegacyVariables = {}; | |
| for (var i in depModules) { | |
| var depModule = depModules[i]; | |
| depModuleLegacyVariables[depModule] = 'jsx.' + depModuleVariables[depModule]; | |
| } | |
| var jquery = 'Libraries/jquery'; | |
| depModules.push(jquery); | |
| depModuleVariables[jquery] = '$'; | |
| depModuleLegacyVariables[jquery] = '$'; | |
| var underscore = 'Libraries/underscore'; | |
| depModules.push(underscore); | |
| depModuleVariables[underscore] = '_'; | |
| depModuleLegacyVariables[underscore] = '_'; | |
| var raphael = 'Libraries/raphael'; | |
| depModules.push(raphael); | |
| depModuleVariables[raphael] = 'Raphael'; | |
| depModuleLegacyVariables[raphael] = 'Raphael'; | |
| var requireModule = 'require'; | |
| depModules.push(requireModule); | |
| depModuleVariables[requireModule] = 'require'; | |
| depModuleLegacyVariables[requireModule] = 'require'; | |
| var depRenameList = {}; | |
| depModules.forEach(function(module) { | |
| depRenameList[module] = module.replace('jsx/', 'Utils/'); | |
| }); | |
| var b = recast.types.builders; | |
| function escape(str) { | |
| return str.replace(/[-[\]{}()*+?.,\\/^$|#\s]/g, '\\$&'); | |
| } | |
| function searchVarExp(str) { | |
| return new RegExp('[^\\d^\\w]' + escape(str) + '[\.\(\s\:\[\,]', 'g'); | |
| } | |
| glob(STATIC_JS_PATH + '!(Libraries|External|Legacy|Tests)/**/*.js', {}, function(error, files) { | |
| files.forEach(function(filepath) { | |
| fs.readFile(filepath, function(error, data) { | |
| if (processFile && filepath.indexOf(processFile) === -1) { | |
| return; | |
| } | |
| console.log('OPENING:' + filepath); | |
| var code = data.toString(); | |
| code = code.replace(/Components\.build\(\s+/, 'Components.build('); | |
| var ast = recast.parse(code); | |
| var firstFunction = ast.program.body[0]; | |
| if (firstFunction && firstFunction.expression) { | |
| firstFunction = firstFunction.expression; | |
| } else { | |
| return; | |
| } | |
| if (!(firstFunction.callee && | |
| firstFunction.callee.name === 'define' && | |
| firstFunction.arguments !== undefined)) { | |
| return; | |
| } | |
| var defineArguments = firstFunction.arguments; | |
| var defineCallback; | |
| defineArguments.forEach(function(arg, index) { | |
| if (arg.type === 'FunctionExpression') { | |
| defineCallback = defineArguments[index]; | |
| } | |
| }); | |
| var componentsBuildArgument; | |
| var buildPath; | |
| // var toNewFormat = false; | |
| var oldStructure; | |
| recast.types.visit(ast, { | |
| visitCallExpression: function(path) { | |
| if (componentsBuildArgument) { | |
| return false; | |
| } | |
| var hasBuild; | |
| try { | |
| hasBuild = path.node.callee.property.name === 'build'; | |
| } catch (ignore) { | |
| this.traverse(path); | |
| return false; | |
| } | |
| if (hasBuild !== true) { | |
| this.traverse(path); | |
| return false; | |
| } | |
| buildPath = path; | |
| componentsBuildArgument = path.node.arguments[0]; | |
| foundBuild(); | |
| return false; | |
| } | |
| }); | |
| function foundBuild() { | |
| var splitedPath = filepath.split(/[^\w]/); | |
| var fileName = splitedPath[splitedPath.length - 2]; | |
| var notInCode = code.match(new RegExp('\\s'+fileName)) === null; | |
| var constructorName = (notInCode && RESERVED_WORDS.indexOf(fileName) === -1) ? fileName : 'Constructor'; | |
| if (componentsBuildArgument.type === 'ObjectExpression') { | |
| console.log('OBJECT FOUND:' + filepath); | |
| return false; | |
| } | |
| // Build в новый формат | |
| var dummy = recast.parse('b({\n defaults: {},\n create: function(element, params) {\n return new '+constructorName+'(element, params);\n }\n});'); | |
| var dummyObject = dummy.program.body[0].expression.arguments[0]; | |
| buildPath.node.arguments[0] = dummyObject; | |
| //dummyObject.properties[1].value = componentsBuildArgument; | |
| var createFunction; | |
| var createViaVar; | |
| var createViaPath; | |
| //var createViaNamedFunc; | |
| // В аргументе функция? | |
| if (componentsBuildArgument.type === 'FunctionExpression') { | |
| createFunction = componentsBuildArgument; | |
| // В аргументе переменная? | |
| } else if (componentsBuildArgument.type === 'Identifier') { | |
| // Ищем переменную | |
| recast.types.visit(ast, { | |
| visitIdentifier: function(path) { | |
| var node = path.node; | |
| if (node.name === componentsBuildArgument.name) { | |
| if (path.parentPath.value.init && path.parentPath.value.init.type === 'FunctionExpression') { | |
| createFunction = path.parentPath.value.init; | |
| //createViaVar = true; | |
| //createViaPath = path; | |
| } | |
| return false; | |
| } | |
| this.traverse(path); | |
| } | |
| }); | |
| // Ищем именованую функцию | |
| if (createFunction === undefined) { | |
| recast.types.visit(ast, { | |
| visitFunctionDeclaration: function(path) { | |
| var node = path.node; | |
| if (node.id.name === componentsBuildArgument.name) { | |
| createFunction = node; | |
| //createViaNamedFunc = true; | |
| //createViaPath = path; | |
| return false; | |
| } | |
| this.traverse(path); | |
| } | |
| }); | |
| } | |
| } | |
| if (createFunction === undefined) { | |
| console.log('NOT FOUND:' + filepath); | |
| return false; | |
| } | |
| // Вешаем аргументы конструктору | |
| createFunction.params = [builder.identifier('element'), builder.identifier('params')]; | |
| // Переносим defaults | |
| var defaultsObject; | |
| recast.types.visit(createFunction, { | |
| visitAssignmentExpression: function(path) { | |
| var node = path.node; | |
| if (node.left.property && node.left.property.name === 'defaults') { | |
| defaultsObject = node.right; | |
| var functionBody = path.parentPath.parentPath.value; | |
| var defaultExpr = path.parentPath.value; | |
| functionBody.splice(functionBody.indexOf(defaultExpr), 1); | |
| return false; | |
| } | |
| this.traverse(path); | |
| } | |
| }); | |
| if (defaultsObject) { | |
| dummyObject.properties[0].value = defaultsObject; | |
| } else { | |
| dummyObject.properties.splice(0, 1); | |
| } | |
| // Отрезаем use strict из кода компонента | |
| createFunction.body.body.forEach(function(func, index) { | |
| if (func.expression && func.expression.value === 'use strict') { | |
| createFunction.body.body.splice(index, 1); | |
| } | |
| }); | |
| var noInit; | |
| // Если у нас только this.init - переносим весь его код в конструктор | |
| if (createFunction.body.body.length === 1 && createFunction.body.body[0].expression.left.property.original.name === 'init') { | |
| createFunction.body.body = createFunction.body.body[0].expression.right.body.body; | |
| noInit = true; | |
| } | |
| // Считаем на каком уровне имеем код\ | |
| // var initIndentRegexp = code.match(/\ +(?=this\.init)/); | |
| // var initIndent = initIndentRegexp && initIndentRegexp[0].length; | |
| // oldStructure = code.match(/define\(\s+/); | |
| var functionConstructor | |
| if (componentsBuildArgument.type === 'FunctionExpression') { | |
| // var createProperty = dummyObject.properties.length === 2 ? dummyObject.properties[1] : dummyObject.properties[0]; | |
| functionConstructor = recast.parse("function "+constructorName+"(element, params) {};\n\n").program.body[0]; | |
| functionConstructor.body.body = createFunction.body.body; | |
| defineCallback.body.body.unshift(functionConstructor); | |
| } else { | |
| var createProperty = dummyObject.properties.length === 2 ? dummyObject.properties[1] : dummyObject.properties[0]; | |
| createProperty.value.body.body[0].argument.callee.name = componentsBuildArgument.name; | |
| functionConstructor = createFunction; | |
| } | |
| //!!!!!!!! | |
| // Если переменная - то надо переименовать конструктор | |
| // if ((createViaNamedFunc || createViaVar) && oldStructure) { | |
| // var createProperty = dummyObject.properties.length === 2 ? dummyObject.properties[1] : dummyObject.properties[0]; | |
| // createFunction.id = null; | |
| // createProperty.value = createFunction; | |
| // if (createViaNamedFunc) { | |
| // var defineBodyCallback = createViaPath.parentPath.value; | |
| // } | |
| // if (createViaVar) { | |
| // var defineBodyCallback = createViaPath.parentPath.parentPath.parentPath.parentPath.value; | |
| // } | |
| // defineBodyCallback.splice(defineBodyCallback.indexOf(createViaPath.node),1); | |
| // } | |
| // if (oldStructure && initIndent === 12) { | |
| // toNewFormat = true; | |
| // } | |
| // Проставляем use strict | |
| defineCallback.body.body.forEach(function(line, index) { | |
| if (line.expression && line.expression.value === 'use strict') { | |
| defineCallback.body.body.splice(index, 1); | |
| } | |
| }); | |
| var useStrictDummy = recast.parse('\'use strict\';\n\n'); | |
| var useStrict = useStrictDummy.program.body[0]; | |
| defineCallback.body.body.unshift(useStrict); | |
| // Добавляем сопутствующий код | |
| var dummyThis = recast.parse('(function() {\n this.params = params;\n this.element = element;\n this.$element = $(element);\n this.init(element, params);\n})'); | |
| var dummyThisArr = dummyThis.program.body[0].expression.body.body; | |
| var createFunctionCode = recast.print(functionConstructor).code; | |
| function findUsage(str) { | |
| return (createFunctionCode.indexOf(str) >= 0 && | |
| createFunctionCode.indexOf(str + ' =') === -1 && | |
| createFunctionCode.indexOf(str + '=') === -1); | |
| } | |
| if (findUsage('this.params')) { | |
| createFunction.body.body.unshift(dummyThisArr[0]); | |
| } | |
| if (findUsage('this.element')) { | |
| createFunction.body.body.unshift(dummyThisArr[1]); | |
| } | |
| if (findUsage('this.$element')) { | |
| createFunction.body.body.unshift(dummyThisArr[2]); | |
| } | |
| if (!noInit) { | |
| createFunction.body.body.push(dummyThisArr[3]); | |
| } | |
| return false; | |
| } | |
| var resultCode = recast.print(ast, { | |
| quote: 'single', | |
| //reuseWhitespace: false, | |
| tabWidth: 4 | |
| }).code; | |
| if (oldStructure) { | |
| resultCode = resultCode.replace(/\n\ {4}/g, '\n'); | |
| resultCode = resultCode.replace(/\n\],\s+function/, '\n], function'); | |
| resultCode = resultCode.replace(/define\(\s+/, 'define('); | |
| resultCode = resultCode.replace(/\n\}\n\)/, '\n})'); | |
| } | |
| var code = resultCode; | |
| code = code.split('jsx/CallBacks').join('jsx/Callbacks'); | |
| code = code.split('jsx.CallBacks').join('jsx.Callbacks'); | |
| var ast = recast.parse(code); | |
| var firstFunction = ast.program.body[0]; | |
| if (firstFunction && firstFunction.expression) { | |
| firstFunction = firstFunction.expression; | |
| } else { | |
| return; | |
| } | |
| if (!(firstFunction.callee && | |
| firstFunction.callee.name === 'define' && | |
| firstFunction.arguments !== undefined)) { | |
| return; | |
| } | |
| var defineArguments = firstFunction.arguments; | |
| // Отрезаем имена в именных модулях | |
| defineArguments.forEach(function(argument, index) { | |
| if (argument.type === 'Literal') { | |
| defineArguments.splice(index, 1); | |
| } | |
| }); | |
| if (defineArguments[0].type === 'FunctionExpression') { | |
| defineArguments[1] = defineArguments[0]; | |
| defineArguments[0] = b.arrayExpression([]); | |
| } | |
| var dependencies = defineArguments[0].elements; | |
| var defineCallback = defineArguments[1]; | |
| var defineCallbackArguments = defineCallback.params; | |
| var bodyCode = recast.print(defineCallback.body).code; | |
| var argsArray = defineCallbackArguments.map(function(param) { | |
| return param.name; | |
| }); | |
| var depsArray = []; | |
| var currentLegacyVariable = {}; | |
| dependencies.forEach(function(dependency, index) { | |
| var value = dependency.value; | |
| depsArray.push(value); | |
| currentLegacyVariable[value] = argsArray[index]; | |
| }); | |
| // Убираем дубликаты в зависимостях | |
| dependencies.forEach(function(dependency, index) { | |
| var value = dependency.value; | |
| if (depsArray.indexOf(value) !== index) { | |
| dependencies.splice(index, 1); | |
| depsArray.splice(index, 1); | |
| if (argsArray[index]) { | |
| argsArray.splice(index, 1); | |
| defineCallbackArguments.splice(index, 1); | |
| } | |
| } | |
| }); | |
| // Ищем пропущенные или лишние зависимости | |
| depModules.forEach(function(depModule) { | |
| var legacyVar = depModuleLegacyVariables[depModule]; | |
| var currentVar = currentLegacyVariable[depModule]; | |
| var hasVarInCode = | |
| (currentVar && (bodyCode.match(searchVarExp(currentVar)) !== null)) || | |
| (legacyVar && (bodyCode.match(searchVarExp(legacyVar)) !== null)); | |
| var theSameModule = filepath.indexOf(depModule) >= 0; | |
| var hasDep = depsArray.indexOf(depModule) !== -1; | |
| var depIndex = depsArray.indexOf(depModule); | |
| // Добавляем пропущенную зависимость | |
| if (hasVarInCode && !hasDep && !theSameModule) { | |
| depsArray.push(depModule); | |
| dependencies.push(b.literal(depModule)); | |
| console.log('ADDED DEP ' + depModule + ':' + filepath); | |
| } | |
| // Убираем лишнюю зависимость | |
| if (!hasVarInCode && hasDep) { | |
| var indexToDelete = depsArray.indexOf(depModule); | |
| depsArray.splice(indexToDelete, 1); | |
| dependencies.splice(indexToDelete, 1); | |
| if (depIndex >= 0 && argsArray[depIndex]) { | |
| argsArray.splice(depIndex, 1); | |
| defineCallbackArguments.splice(depIndex, 1); | |
| } | |
| console.log('REMOVED DEP ' + depModule + ':' + filepath); | |
| } | |
| }); | |
| // добавляем переменные | |
| for (var i in depModules) { | |
| var depModule = depModules[i]; | |
| var depIndex = depsArray.indexOf(depModule); | |
| if (depIndex >= 0 && argsArray[depIndex] === undefined) { | |
| defineCallbackArguments[depIndex] = b.identifier(depModuleVariables[depModule]); | |
| console.log('ADDED VAR ' + depModuleVariables[depModule] + ':' + filepath); | |
| } | |
| } | |
| // Сортируем зависимости | |
| var order = ['require', 'Libraries', 'HHC', 'Utils', 'jsx']; | |
| dependencies.forEach(function(dep, index) { | |
| dep._oldIndex = index; | |
| }); | |
| dependencies.sort(function(a, b) { | |
| // Зависимости без переменной - всегда в конец | |
| if (defineCallbackArguments[a._oldIndex] === undefined) { | |
| return 2; | |
| } | |
| if (defineCallbackArguments[a._oldIndex] === undefined && defineCallbackArguments[b._oldIndex] === undefined) { | |
| return 0; | |
| } | |
| if (defineCallbackArguments[b._oldIndex] === undefined) { | |
| return -1; | |
| } | |
| var aSpace = a.value.split('/')[0]; | |
| var bSpace = b.value.split('/')[0]; | |
| return (order.indexOf(aSpace) === -1 ? order.length : order.indexOf(aSpace)) - | |
| (order.indexOf(bSpace) === -1 ? order.length : order.indexOf(bSpace)); | |
| }); | |
| var newDefineCallbackArguments = []; | |
| dependencies.forEach(function(dep, index) { | |
| if (defineCallback.params[dep._oldIndex] !== undefined) { | |
| newDefineCallbackArguments[index] = defineCallback.params[dep._oldIndex]; | |
| } | |
| }); | |
| defineCallback.params = newDefineCallbackArguments; | |
| defineCallbackArguments = defineCallback.params; | |
| if (dependencies.length === 0) { | |
| defineArguments.splice(0, 1); | |
| } | |
| // заменяем HH на window | |
| recast.types.visit(ast, { | |
| visitIdentifier: function(path) { | |
| var node = path.node; | |
| if (node.name === 'HH') { | |
| node.name = 'window'; | |
| } | |
| this.traverse(path); | |
| } | |
| }); | |
| var preRenameCode = recast.print(ast, { | |
| quote: 'single', | |
| reuseWhitespace: false | |
| }).code; | |
| var resultCode = preRenameCode; | |
| // переименовываем легаси переменные | |
| depModules.forEach(function(depModule) { | |
| var currentVar = currentLegacyVariable[depModule]; | |
| var legacyVar = depModuleLegacyVariables[depModule]; | |
| var hasVarInCode = | |
| (currentVar && (resultCode.match(searchVarExp(currentVar)) !== null)) || | |
| (legacyVar && (resultCode.match(searchVarExp(legacyVar)) !== null)); | |
| if (hasVarInCode) { | |
| if (currentVar) { | |
| resultCode = resultCode.replace(new RegExp('([^\\w^\\d])' + escape(currentVar) + '(?![\\d\\w])', 'g'), '$1' + depModuleVariables[depModule]); | |
| } | |
| if (legacyVar) { | |
| resultCode = resultCode.replace(new RegExp('([^\\w^\\d])' + escape(legacyVar) + '(?![\\d\\w])', 'g'), '$1' + depModuleVariables[depModule]); | |
| } | |
| if (preRenameCode !== resultCode) { | |
| console.log('REMOVED OLD VARS:' + currentVar + ' ' + legacyVar + ' ' + filepath); | |
| } | |
| } | |
| }); | |
| resultCode = resultCode.split('Libraries/').join(''); | |
| // Переименовываем jsx -> Utils | |
| Object.keys(depRenameList).forEach(function(dep) { | |
| resultCode = resultCode.split(dep).join(depRenameList[dep]); | |
| }); | |
| // форматируем завимости | |
| if (!resultCode.match(/define\(\[\n/)) { | |
| var toReplace = resultCode.match(/define\(\[[^\]]+/); | |
| if (toReplace) { | |
| var newString = toReplace[0].split(', ').join(',').replace(/('[\w/]+')/g, "\n $1") + '\n'; | |
| resultCode = resultCode.split(toReplace[0]).join(newString); | |
| } | |
| } | |
| fs.writeFile(filepath, resultCode); | |
| }); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment