Skip to content

Instantly share code, notes, and snippets.

@demmer
Created February 5, 2016 01:12
Show Gist options
  • Save demmer/562cfc2343cc9d049b69 to your computer and use it in GitHub Desktop.
Save demmer/562cfc2343cc9d049b69 to your computer and use it in GitHub Desktop.
converter script to translate extendable-base classes into ES6 classes
#!/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