Operation JavaScript Transformation
About me
@BrianDiPalma1
https://github.com/briandipalma/global-compiler
https://github.com/briandipalma/gc-cli
A little history...
We started writing large JS web apps 8+ years ago.
jQuery was in its infancy.
Before AMD.
Before CJS.
Before ES6 modules.
...so we copied what we knew
And we knew Java.
novox.fxtrader.grid.decorator.MyDecorator = function(mDecoratorConfig)
{
this.clickedClass = mDecoratorConfig.clickedClass;
};
caplin.implement(novox.fxtrader.grid.decorator.MyDecorator, caplin.grid.GridViewListener);
// GridDecorator Interface Methods
novox.fxtrader.grid.decorator.MyDecorator.prototype.setGridView = function(oGridView)
{
// register the decorator as a listener
this.m_oGridView = oGridView;
oGridView.addGridViewListener(this);
};
novox.fxtrader.grid.decorator.MyDecorator.prototype._onClick = function(oEvent)
{
oEvent = caplin.dom.event.Event.getNormalizedEvent(oEvent);
var eRow = caplin.dom.Utility.getAncestorElementWithClass(oEvent.target, "row");
if(caplin.dom.Utility.hasClassName(eRow, this.clickClass)) {
caplin.dom.Utility.removeClassName(eRow, this.clickClass);
} else {
caplin.dom.Utility.addClassName(eRow, this.clickClass);
}
};
Custom Loader
Code was loaded by a program that scanned the source code for strings that matched known class names.
If it found a match in a file it would include the matched file.
At that point all was good...
A wild MODULE appeared!
Eventually the JS community solved the code structure and loading problem too.
They devised AMD and CJS.
These were used as inspiration for the ES6 module specification.
Modules are the future
That combined with the readability improvements of using CJS convinced us to add support for CJS.
The plan was to use CJS for new code while allowing us to still use our current code.
No one likes legacy code
Maybe we could make all the code look like the shiny and new code?
Giving us more readable code and consistent style and mental model.
Could we convert the old style code?
Approaches
- Manual conversion
- Regular expressions
- Find and replace
- Parse and mutate the code
Approaches
-
Manual conversionLaborious, repetitive work is error prone, can't justify cost/time -
Regular expressionsError prone, inflexible, complexity of edge cases. -
Find and replaceError prone, inflexible, complexity of edge cases. - Parse and mutate the code
novox.fxtrader.grid.decorator.MyDecorator = function(mDecoratorConfig)
{
this.clickedClass = mDecoratorConfig.clickedClass;
};
caplin.implement(novox.fxtrader.grid.decorator.MyDecorator, caplin.grid.GridViewListener);
// GridDecorator Interface Methods
novox.fxtrader.grid.decorator.MyDecorator.prototype.setGridView = function(oGridView)
{
// register the decorator as a listener
this.m_oGridView = oGridView;
oGridView.addGridViewListener(this);
};
novox.fxtrader.grid.decorator.MyDecorator.prototype._onClick = function(oEvent)
{
oEvent = caplin.dom.event.Event.getNormalizedEvent(oEvent);
var eRow = caplin.dom.Utility.getAncestorElementWithClass(oEvent.target, "row");
if(caplin.dom.Utility.hasClassName(eRow, this.clickClass)) {
caplin.dom.Utility.removeClassName(eRow, this.clickClass);
} else {
caplin.dom.Utility.addClassName(eRow, this.clickClass);
}
};
Convert this
var topiarist = require('topiarist');
var Utility = require('caplin/dom/Utility');
var Event = require('caplin/dom/event/Event');
var GridViewListener = require('caplin/grid/GridViewListener');
function MyDecorator(mDecoratorConfig) {
this.clickedClass = mDecoratorConfig.clickedClass;
};
topiarist.implement(MyDecorator, GridViewListener);
// GridDecorator Interface Methods
MyDecorator.prototype.setGridView = function(oGridView) {
// register the decorator as a listener
this.m_oGridView = oGridView;
oGridView.addGridViewListener(this);
};
MyDecorator.prototype._onClick = function(oEvent) {
oEvent = Event.getNormalizedEvent(oEvent);
var eRow = Utility.getAncestorElementWithClass(oEvent.target, "row");
if(Utility.hasClassName(eRow, this.clickClass)) {
Utility.removeClassName(eRow, this.clickClass);
} else {
Utility.addClassName(eRow, this.clickClass);
}
};
module.exports = MyDecorator;
to this
Parsers and ASTs
- Esprima
- Acorn
- Espree
- Babylon
They all output ESTree compliant ASTs
var ast = esprima.parse('var answer = 42', options);
ASTs
Data structure representing abstract syntactic code structure.
Doesn't keep track of data that has no
execution semantics.
So code like this...
my.long.name.space.Field
is represented like...
"type": "MemberExpression",
"computed": false,
"object": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Identifier",
"name": "my"
},
"property": {
"type": "Identifier",
"name": "long"
}
},
"property": {
"type": "Identifier",
"name": "name"
}
},
"property": {
"type": "Identifier",
"name": "space"
}
},
"property": {
"type": "Identifier",
"name": "Field"
}
Identifier
An identifier. Note that an identifier may be an expression or a destructuring pattern.
interface Identifier <: Node, Expression, Pattern {
type: "Identifier";
name: string;
}
Node types
http://esprima.org/demo/parse.html
http://eslint.org/parser/
http://astexplorer.net/
Transforming ASTs
Recast
https://github.com/benjamn/recast
import {parse, types, print} from 'recast';
// Parse the code using an interface similar to esprima.parse.
const ast = parse('my.long.name.space.Field');
// Recast allows you to safely build new AST nodes.
const moduleIdentifier = types.builders.identifier('Field');
// You can modify the AST by replacing the "ExpressionStatement" node
// "expression" property with your newly created "Identifier".
ast.program.body[0].expression = moduleIdentifier;
// Field
print(ast).code;
import {visit} from 'recast';
...
const flatteningVisitor = {
visitIdentifier(path) {
// do something with path
this.traverse(path);
},
visitExpressionStatement(path) {
// do something with path
this.traverse(path);
}
};
visit(ast, flatteningVisitor);
Traversing ASTs
You need to find the nodes to transform.
Visitor pattern is the standard approach.
...
visitIdentifier(identifierNodePath) {
const {parent} = identifierNodePath;
// Is this identifier the leaf a fully qualified name.
if (isClassNamespaceLeaf(identifierNodePath, parent, this._namespaceList)) {
// Recast allows you to safely build new AST nodes.
const classIdentifier = types.builders.identifier(this._className);
// It allows you to replace AST nodes.
identifierNodePath.replace(classIdentifier);
}
this.traverse(path);
},
...
NodePaths
What Recast provides to the visitor callbacks.
Provide API to mutate underlying AST e.g. replace, prune, unshift, shift, etc.
Also provides scope information e.g. isGlobal, declares
// Matcher that matches `caplin.extend()` or `caplin.implement()`
const caplinInheritanceMatcher = composeMatchers(
identifierMatcher('caplin'),
orMatchers(
memberExpressionMatcher({property: identifierMatcher('extend')}),
memberExpressionMatcher({property: identifierMatcher('implement')})
),
callExpressionMatcher()
);
// Transformer that converts caplin.extend/implement to topiarist.extend
const caplinInheritanceToExtendTransformer = composeTransformers(
identifier('topiarist'),
extractParent(),
extractProperties('property'),
identifier('extend')
);
// Matchers and transforms are passed into a generic visitor
Functional composition
Some transforms can be described as static matcher and transform pairs.
Multiple Transforms
export function compileSourceFiles(options) {
vinylFs.src('src/**/*.js')
.pipe(parseJSFile())
.pipe(expandVarNamespaceAliases(options.namespaces))
.pipe(flattenIIFEClass())
.pipe(flattenClass())
.pipe(convertGlobalsToRequires(options.namespaces))
.pipe(addRequiresForLibraries(options.libraryIdentifiersToRequire))
.pipe(transformI18nUsage())
.pipe(convertASTToBuffer())
.pipe(vinylFs.dest(options.outputDirectory))
.on('end', createJSStyleFiles());
}
Keep your transforms as small and focused as you can. Helps deal with myriad of code patterns in real world.
Make your transforms configurable.
export const namespacedClassFlattenerVisitor = {
/**
* @param {string} fullyQualifiedName The fully qualified class name
*/
initialize(fullyQualifiedName) {
const nameParts = fullyQualifiedName.split('.').reverse();
this._namespaceList = List.of(...nameParts);
this._className = this._namespaceList.first();
}
...
jscodeshift
For one-off, stand alone transforms.
$ jscodeshift --transform myTransform.js src/.
/**
* This replaces every occurence of variable "foo".
*/
module.exports = function(fileInfo, api) {
return api.jscodeshift(fileInfo.source)
.findVariableDeclarators('foo')
.renameTo('bar')
.toSource();
}
Provides access to recast builders.
Supports stats/dry runs and template literals.
js-codemod has sample transforms.
// Searching for all Identifiers.
jscodeshift(input)
.find(jscodeshift.Identifier)
.forEach(function(path) {
// do something with path
});
// How to use Template Literals.
jscodeshift(input)
.find('ForStatement')
.replaceWith(
({node}) => statements`
${node.init};
while (${node.test}) {
${node.body.body}
${node.update}
}`
)
.toSource();
examples of the jscodeshift API
$ jscodeshift -t myTransforms fileA fileB --foo=bar
Also supports passing in CLI options.
Babel Plugin
export default function ({ Plugin, types: t }) {
return new Plugin("foo-bar", {
visitor: {
FunctionDeclaration(node, parent, scope) {
this.parentPath.replaceWith(
t.expressionStatement(t.literal("no function declarations allowed!"))
);
if (scope.hasOwnBinding("name")) {
// "name" bound in this scope.
}
}
}
});
}
Useful for build time, reusable transforms.
Rich API e.g. replace with multiple nodes, with source string,
rename bindings, visitor aliases (Function for FunctionDeclaration/Expression), unique identifier creation.
Enough with the Abstract!
What about style guides?
Linter rules?
Recast can have its output configured but it's not very flexible.
esformatter
Highly configurable for indentation and white space
{
"indent": {
"value": "\t",
"FunctionExpression": 1,
"ArrayExpression": 0,
"ObjectExpression": 0
}
}
Plugins can be used for other types formatting changes.
- esformatter-quotes
- esformatter-spaced-lined-comment
- esformatter-quote-props
module.exports.nodeBefore = function(node) {
if (isQuotedProperty(node) && isSafeToUnquote(node)) {
unquoteProperty(node);
}
};
Operate on source string, tokens or on AST nodes
- setOptions
- stringBefore
- transformBefore
- tokenBefore
- nodeBefore
- nodeAfter
- tokenAfter
- transformAfter
- stringAfter
module.exports.tokenBefore = function(token) {
if (token.type === 'LineComment' && token.value.match(/^\S/)) {
token.raw = '// ' + token.value;
}
};
Congratulations!
You have transformed an old code base into a modern code base that follows your code style guide, passes linting checks.
Don't be afraid to contemplate huge changes to your code base.
Questions?
JavaScript Tran
By briandipalma
JavaScript Tran
- 1,503