Have you ever thought, “I wish it was easier to change JavaScript code programmatically?” Maybe you wanted to write or edit a configuration block in source code. Perhaps you wanted to generate customized algorithmic code. For many, this kind of thing seems inaccessible.
The tools exist, though. In this talk, Stephen Vance will look at how he has used recast and esprima to edit and rewrite JavaScript code, leaving the untouched code completely intact, including whitespace and comments. At the end, you should have enough knowledge to be dangerous and start to write the next automatic programming, AI, take-over-the-world, self-improving software.
2. Ember.js Routes
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
});
export default Router;
2
3. Ember Route Generator
$ ember generate route foo
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('foo');
});
export default Router;
3
4. Bug: Route Removal
$ ember destroy route bar
Router.map(function() {
this.route('bar');
this.route('foo', function() {
this.route('bar');
});
});
export default Router;
Fixed in Fix like-named route removal bug
4
5. The Tools
Esprima
ECMAScript parsing
infrastructure for multipurpose
analysis
Site: http://esprima.org/
by Ariya Hidayat
recast
JavaScript syntax tree
transformer, nondestructive
pretty-printer, and automatic
source map generator
by Ben Newman
5
8. Using recast
const recast = require('recast');
const fs = require('fs');
const source = fs.readFileSync('some-code.js', 'utf-8');
let ast = recast.parse(source);
// Manipulate ast
let output = recast.print(ast).code;
8
9. A Real-Life Use
// ember-cli-build.js
module.exports = function(defaults) {
var app = new EmberApp(defaults, {
// Add options here
});
return app.toTree();
};
9
Examples taken from ember-cli-build-config-editor
10. Finding a Marker
recast.visit(ast, {
visitNewExpression: function (path) {
var node = path.node;
if (node.callee.name === 'EmberApp') {
editor.configNode = node.arguments.find(isObjectExpression);
return false;
} else {
this.traverse(path);
}
}
})
10
11. Adding Syntax
function findOrAddConfigKey(key) {
var configKey = this.configNode.properties.find(isKey(key));
if (!configKey) {
configKey = builders.property(
'init',
builders.literal(key),
builders.objectExpression([])
);
this.configNode.properties.push(configKey);
}
return configKey;
}
11
12. What Type Is It?
function isKey(key) {
return function (property) {
return (property.key.type === 'Literal'
&& property.key.value === key)
|| (property.key.type === 'Identifier'
&& property.key.name === key);
};
}
12
13. Now we have …
module.exports = function(defaults) {
var app = new EmberApp(defaults, {
'some-addon': {
}
});
return app.toTree();
};
13
14. Editing Simple Values
function addOrUpdateConfigProperty(configObject, property, config) {
var existingProperty = configObject.properties.find(isKey(property));
if (existingProperty) {
existingProperty.value.value = config[property];
} else {
var newProperty = builders.property(
'init',
builders.literal(property),
builders.literal(config[property])
);
configObject.properties.push(newProperty);
}
}
14
15. The Result
module.exports = function(defaults) {
var app = new EmberApp(defaults, {
'some-addon': {
'booleanProperty': false,
'numericProperty': 17,
'stringProperty': 'wow'
}
});
return app.toTree();
};
15
16. Adding a Function Call
var node = builders.expressionStatement(
builders.callExpression(
builders.memberExpression(
builders.thisExpression(),
builders.identifier(options.identifier || 'route'),
false
),
[builders.literal(name)]
)
);
16
Example from ember-router-generator