@BrianDiPalma1
https://github.com/briandipalma/global-compiler
https://github.com/briandipalma/gc-cli
We started writing large JS web apps 8+ years ago.
jQuery was in its infancy.
Before AMD.
Before CJS.
Before ES6 modules.
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);
}
};
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...
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.
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.
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?
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);
}
};
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;
They all output ESTree compliant ASTs
var ast = esprima.parse('var answer = 42', options);
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"
}
An identifier. Note that an identifier may be an expression or a destructuring pattern.
interface Identifier <: Node, Expression, Pattern {
type: "Identifier";
name: string;
}
http://esprima.org/demo/parse.html
http://eslint.org/parser/
http://astexplorer.net/
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);
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);
},
...
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
Some transforms can be described as static matcher and transform pairs.
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();
}
...
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.
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.
What about style guides?
Linter rules?
Recast can have its output configured but it's not very flexible.
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.
module.exports.nodeBefore = function(node) {
if (isQuotedProperty(node) && isSafeToUnquote(node)) {
unquoteProperty(node);
}
};
Operate on source string, tokens or on AST nodes
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?