Created
February 5, 2016 01:12
-
-
Save demmer/562cfc2343cc9d049b69 to your computer and use it in GitHub Desktop.
converter script to translate extendable-base classes into ES6 classes
This file contains 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 | |
// | |
// Script to convert any extendable-base derived classes in the specified | |
// files into proper ES6 classes. | |
// | |
// Along the way it will remove any require() statements for extendable-base | |
// itself, convert any prototype literals into ES6 properties, and add the | |
// appropriate superclass constructor invocations to get the same behavior as | |
// the extendable-base initialize chain. | |
// | |
// Requires a recent version of node and the `recast` library. | |
var recast = require('recast'); | |
var fs = require('fs'); | |
if (process.argv.length < 2) { | |
console.error('usage:', process.argv[1], '<file1> <file2> ...'); | |
process.exit(1); | |
} | |
process.argv.slice(2).forEach(convert); | |
function debug() { | |
if (process.env.DEBUG) { | |
console.log.apply(console, arguments); | |
} | |
} | |
function dump(object) { | |
console.log(JSON.stringify(object, null, 4)); | |
} | |
function convert(file) { | |
if (fs.statSync(file).isDirectory()) { | |
return; | |
} | |
console.log('converting', file, '...') | |
var code = fs.readFileSync(file).toString(); | |
var ast = recast.parse(code); | |
// Get the given nested property from the specified object. | |
// Returns undefined if any element of the path doesn't exist. | |
function get(object, nested) { | |
var L = nested.split('.'); | |
L.forEach(function(property) { | |
if (object && object[property]) { | |
object = object[property]; | |
} else { | |
object = undefined; | |
} | |
}); | |
debug('get', nested, '=>', object) | |
return object; | |
} | |
// First look through the top level statements for an expression of the form | |
// var Base = require('extendable-base'); | |
// | |
// If found, remove it from the program and stash the variable into which | |
// it was required so we can tell the difference between a class that simply | |
// extends base vs one that extends another class. | |
var baseVar; | |
for (var i = 0; i < ast.program.body.length; ++i) { | |
var expr = ast.program.body[i]; | |
if (expr.type === 'VariableDeclaration' && | |
get(expr.declarations[0], 'init.callee.name') === 'require' && | |
get(expr.declarations[0], 'init.arguments.0.value') === 'extendable-base') | |
{ | |
if (expr.comments) { | |
var next = ast.program.body[i + 1]; | |
if (next.comments) { | |
next.comments = expr.comments.concat(next.comments); | |
} else { | |
next.comments = expr.comments; | |
} | |
} | |
delete ast.program.body[i]; | |
baseVar = expr.declarations[0].id.name; | |
} | |
} | |
if (baseVar) { | |
debug('found extendable-base', baseVar); | |
} | |
// Then normalize the use of Base.inherits to convert | |
// var Class = Base.inherits(Superclass, {...}) | |
// | |
// into the more canonical form | |
// var Class = Superclass.extend({...}); | |
// | |
// This way it gets picked up properly by the checks below. | |
for (var i = 0; i < ast.program.body.length; ++i) { | |
var expr = ast.program.body[i]; | |
if (get(expr, 'type') === 'VariableDeclaration' && | |
get(expr.declarations[0], 'init.callee.type') === 'MemberExpression' && | |
get(expr.declarations[0], 'init.callee.property.name') === 'inherits' && | |
get(expr.declarations[0], 'init.arguments.1.type') === 'ObjectExpression') | |
{ | |
expr.declarations[0].init.callee.object = expr.declarations[0].init.arguments[0]; | |
expr.declarations[0].init.callee.property.name = 'extend'; | |
expr.declarations[0].init.arguments.shift(); | |
} | |
} | |
// Then look through all the other top level statements for an extendable | |
// base style class definition of the form: | |
// | |
// var Classname = Superclass.extend({}); | |
for (var i = 0; i < ast.program.body.length; ++i) { | |
var expr = ast.program.body[i]; | |
if (get(expr, 'type') !== 'VariableDeclaration' || | |
get(expr.declarations[0], 'init.callee.type') !== 'MemberExpression' || | |
get(expr.declarations[0], 'init.callee.property.name') !== 'extend') | |
{ | |
continue; | |
} | |
var propertyType = get(expr.declarations[0], 'init.arguments.0.type'); | |
if (propertyType !== 'ObjectExpression') { | |
throw new Error('invalid extends invocation: "' + recast.print(expr).code + '"'); | |
} | |
debug('found class', expr.declarations[0].id.name) | |
var decl = { | |
type: 'ClassDeclaration', | |
loc: expr.declarations[0].loc, | |
id: expr.declarations[0].id, | |
comments: expr.comments | |
}; | |
// Set the superclass if it's not directly extendable-base | |
var superClass = expr.declarations[0].init.callee.object; | |
if (!baseVar || (superClass.name !== baseVar)) { | |
decl.superClass = superClass; | |
} | |
// Mapping function to convert a prototype or class property into a | |
// method, either instance or static. | |
var isStatic = false; | |
function convert(property) { | |
// Since ES6 classes don't support literal values in the prototype, | |
// convert them into get properties. | |
if (property.value.type === 'Literal' || | |
property.value.type === 'Identifier' || | |
property.value.type === 'ObjectExpression') { | |
return { | |
type: 'MethodDefinition', | |
key: property.key, | |
comments: property.comments, | |
value: { | |
type: 'FunctionExpression', | |
'id': null, | |
'params': [], | |
'defaults': [], | |
body: { | |
type: 'BlockStatement', | |
body: [ | |
{ | |
type: 'ReturnStatement', | |
argument: property.value | |
} | |
] | |
} | |
}, | |
kind: 'get', | |
static: isStatic | |
} | |
} | |
else if (property.value.type === 'FunctionExpression') { | |
var method = property; | |
method.type = 'MethodDefinition'; | |
// Convert initialize(args) into constructor(args) and add the | |
// superclass constructor invocation, passing through all the | |
// arguments from the initialize function. | |
if (method.key.name === 'initialize') { | |
method.key.name = 'constructor'; | |
method.kind = 'constructor'; | |
if (superClass.name !== baseVar) { | |
var superConstructor = { | |
type: 'ExpressionStatement', | |
expression: { | |
type: 'CallExpression', | |
callee: { | |
type: 'Super' | |
}, | |
arguments: method.value.params | |
} | |
}; | |
method.value.body.body.unshift(superConstructor) | |
} | |
} | |
method.static = isStatic; | |
return method; | |
} else { | |
throw new Error('unhandled value type ' + JSON.stringify(property, null, 4)); | |
} | |
} | |
var properties = expr.declarations[0].init.arguments[0].properties; | |
decl.body = { | |
type: 'ClassBody', | |
body: properties.map(convert, false) | |
}; | |
if (expr.declarations[0].init.arguments.length === 2) { | |
isStatic = true; | |
var staticProperties = expr.declarations[0].init.arguments[1].properties | |
staticProperties = staticProperties.map(convert, true); | |
decl.body.body = decl.body.body.concat(staticProperties); | |
} | |
ast.program.body[i] = decl; | |
} | |
code = recast.print(ast).code | |
fs.writeFileSync(file, code); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment