@qooxdoo/framework
Version:
The JS Framework for Coders
464 lines (424 loc) • 13.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2021 Zenesis Ltd
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* John Spackman (john.spackman@zenesis.com, @johnspackman)
************************************************************************ */
const fs = require("fs");
const babelCore = require("@babel/core");
const types = require("@babel/types");
const prettier = require("prettier");
/**
* Helper method that collapses the MemberExpression into a string
* @param node
* @returns {string}
*/
function collapseMemberExpression(node) {
var done = false;
function doCollapse(node) {
if (node.type == "ThisExpression") {
return "this";
}
if (node.type == "Identifier") {
return node.name;
}
if (node.type == "ArrayExpression") {
var result = [];
node.elements.forEach(element => result.push(doCollapse(element)));
return result;
}
if (node.type != "MemberExpression") {
return "(" + node.type + ")";
}
if (types.isIdentifier(node.object)) {
let str = node.object.name;
if (node.property.name) {
str += "." + node.property.name;
} else {
done = true;
}
return str;
}
var str;
if (node.object.type == "ArrayExpression") {
str = "[]";
} else {
str = doCollapse(node.object);
}
if (done) {
return str;
}
// `computed` is set if the expression is a subscript, eg `abc[def]`
if (node.computed) {
done = true;
} else if (node.property.name) {
str += "." + node.property.name;
} else {
done = true;
}
return str;
}
return doCollapse(node);
}
/**
* Processes a .js source file and tries to upgrade to ES6 syntax
*
* This is a reliable but fairly unintrusive upgrade, provided that `arrowFunctions` property is
* `careful`. The issue is that this code: `setTimeout(function() { something(); })` can be
* changed to `setTimeout(() => something())` and that is often desirable, but it also means that
* the `this` will be different because an arrow function always has the `this` from where the
* code is written.
*
* However, if you use an API which changes `this` then the switch to arrow functions will break
* your code. Mostly, in Qooxdoo, changes to `this` are done via an explicit API (eg
* `obj.addListener("changeXyx", function() {}, this)`) and so those known APIs can be translated,
* but there are places which do not work this way (eg the unit tests `qx.dev.unit.TestCase.resume()`).
* Third party integrations are of course completely unknown.
*
* If `arrowFunctions` is set to aggressive, then all functions are switched to arrow functions except
* where there is a known API that does not support it (eg any call to `.resume` in a test class); this
* could break your code.
*
* If `arrowFunctions is set to `careful` (the default), then functions are only switched to arrow
* functions where the API is known (eg `.addListener`).
*
* The final step is that the ES6ify will use https://prettier.io/ to reformat the code, and will use
* the nearest `prettierrc.json` for configuration
*/
qx.Class.define("qx.tool.compiler.Es6ify", {
extend: qx.core.Object,
construct(filename) {
super();
this.__filename = filename;
this.__knownApiFunctions = ["addListener", "addListenerOnce"];
},
properties: {
/** Whether to convert functions to arrow functions; careful means only on things like addListener callbacks */
arrowFunctions: {
init: "careful",
check: ["never", "always", "careful", "aggressive"],
nullable: true
},
/** Whether to force braces around single line bodies for if, for, while, and do while */
singleLineBlocks: {
init: false,
check: "Boolean"
},
/** Whether to overwrite the original file */
overwrite: {
init: false,
check: "Boolean"
}
},
members: {
/** @type{String} the filename to work on */
__filename: null,
/** @type{} */
__knownApiFunctions: null,
/**
* Transforms the named file
*/
async transform() {
let src = await fs.promises.readFile(this.__filename, "utf8");
let babelConfig = {};
let options = qx.lang.Object.clone(babelConfig.options || {}, true);
options.modules = false;
let plugins = [
require("@babel/plugin-syntax-jsx"),
this.__pluginFunctionExpressions()
];
if (this.getArrowFunctions() != "never") {
plugins.push(this.__pluginArrowFunctions());
}
plugins.push(this.__pluginRemoveUnnecessaryThis());
plugins.push(this.__pluginSwitchToSuper());
if (this.getSingleLineBlocks()) {
plugins.push(this.__pluginSingleLineBlocks());
}
var config = {
ast: true,
babelrc: false,
sourceFileName: this.__filename,
filename: this.__filename,
sourceMaps: false,
presets: [
[
{
plugins: plugins
}
]
],
parserOpts: {
allowSuperOutsideMethod: true,
sourceType: "script"
},
generatorOpts: {
retainLines: true,
compact: false
},
passPerPreset: true
};
let result;
let cycleCount = 0;
while (true) {
cycleCount++;
if (cycleCount > 10) {
qx.tool.compiler.Console.warn(
`Can not find a stable format for ${this.__filename}`
);
break;
}
result = babelCore.transform(src, config);
if (result.code === src) {
break;
}
src = result.code;
}
let prettierConfig =
(await prettier.resolveConfig(this.__filename, {
editorConfig: true
})) || {};
prettierConfig.parser = "babel";
let prettyCode = prettier.format(result.code, prettierConfig);
let outname = this.__filename + (this.isOverwrite() ? "" : ".es6ify");
await fs.promises.writeFile(outname, prettyCode, "utf8");
},
/**
* Plugin that converts object properties which are functions into object methods, eg
* ```
* {
* myMethod: function() {}
* }
* ```
* becomes
* ```
* {
* myMethod() {}
* }
* ```
* @returns
*/
__pluginFunctionExpressions() {
return {
visitor: {
ObjectExpression(path) {
const KEY_TYPES = {
Identifier: 1,
StringLiteral: 1,
NumericLiteral: 1
};
for (let i = 0; i < path.node.properties.length; i++) {
let propNode = path.node.properties[i];
if (
propNode.type == "ObjectProperty" &&
propNode.value.type == "FunctionExpression" &&
KEY_TYPES[propNode.key.type]
) {
let replacement = types.objectMethod(
"method",
propNode.key,
propNode.value.params,
propNode.value.body,
propNode.value.computed,
propNode.value.generator,
propNode.value.async
);
replacement.loc = propNode.loc;
replacement.start = propNode.start;
replacement.end = propNode.end;
replacement.leadingComments = propNode.leadingComments;
path.node.properties[i] = replacement;
}
}
}
}
};
},
/**
* Converts a function expression into an arrow function expression
*
* @param {Babel.Node} argNode
* @returns
*/
__toArrowExpression(argNode) {
let body = argNode.body;
if (body.body.length == 1 && body.body[0].type == "ReturnStatement") {
body = body.body[0].argument;
}
let replacement = types.arrowFunctionExpression(
argNode.params,
body,
argNode.async
);
replacement.loc = argNode.loc;
replacement.start = argNode.start;
replacement.end = argNode.end;
replacement.leadingComments = argNode.leadingComments;
return replacement;
},
/**
* Plugin that makes sure that every single line block is wrapped in braces
*
* @returns
*/
__pluginSingleLineBlocks() {
function loopStatement(path) {
if (path.node.body.type == "BlockStatement") {
return;
}
let block = types.blockStatement([path.node.body]);
path.node.body = block;
}
return {
visitor: {
IfStatement(path) {
if (path.node.consequent.type == "BlockStatement") {
return;
}
let block = types.blockStatement([path.node.consequent]);
path.node.consequent = block;
},
DoWhileStatement: loopStatement,
ForStatement: loopStatement,
WhileStatement: loopStatement
}
};
},
/**
* Tries to convert functions into arrow functions
* @returns
*/
__pluginArrowFunctions() {
let t = this;
const isTest = this.__filename.indexOf("/test/") > -1;
let arrowFunctions = this.getArrowFunctions();
let knownApiFunctions = this.__knownApiFunctions;
return {
visitor: {
CallExpression(path) {
if (path.node.callee.type == "MemberExpression") {
let callee = collapseMemberExpression(path.node.callee);
if (arrowFunctions == "careful") {
if (
!knownApiFunctions.some(fName => callee.endsWith("." + fName))
) {
return;
}
if (
path.node.arguments.length != 3 ||
path.node.arguments[0].type != "StringLiteral" ||
path.node.arguments[1].type != "FunctionExpression" ||
path.node.arguments[2].type != "ThisExpression"
) {
return;
}
} else if (arrowFunctions == "aggressive") {
if (
callee == "qx.event.GlobalError.observeMethod" ||
callee == "this.assertException" ||
callee == "this.assertEventFired" ||
callee == "qx.core.Assert.assertEventFired" ||
(isTest && callee.endsWith(".resume"))
) {
return;
}
}
} else if (arrowFunctions == "careful") {
return;
}
for (let i = 0; i < path.node.arguments.length; i++) {
let argNode = path.node.arguments[i];
if (argNode.type == "FunctionExpression") {
path.node.arguments[i] = t.__toArrowExpression(argNode);
}
}
}
}
};
},
/**
* Where a function has been translated into an arrow function, the this binding is not needed
* and can be removed
* @returns
*/
__pluginRemoveUnnecessaryThis() {
let knownApiFunctions = this.__knownApiFunctions;
return {
visitor: {
CallExpression(path) {
if (
path.node.callee.type == "MemberExpression" &&
path.node.callee.property.type == "Identifier" &&
knownApiFunctions.includes(path.node.callee.property.name) &&
path.node.arguments.length == 3 &&
path.node.arguments[0].type == "StringLiteral" &&
path.node.arguments[1].type == "ArrowFunctionExpression" &&
path.node.arguments[2].type == "ThisExpression"
) {
qx.lang.Array.removeAt(path.node.arguments, 2);
}
}
}
};
},
/**
* Translates `this.base(arguments...)` into `super`
* @returns
*/
__pluginSwitchToSuper() {
let methodNameStack = [];
function peekMethodName() {
for (let i = methodNameStack.length - 1; i >= 0; i--) {
let methodName = methodNameStack[i];
if (methodName) {
return methodName;
}
}
return null;
}
return {
visitor: {
ObjectMethod: {
enter(path) {
methodNameStack.push(path.node.key.name || null);
},
exit(path) {
methodNameStack.pop();
}
},
CallExpression(path) {
if (
path.node.callee.type == "MemberExpression" &&
path.node.callee.object.type == "ThisExpression" &&
path.node.callee.property.type == "Identifier" &&
path.node.callee.property.name == "base" &&
path.node.arguments.length >= 1
) {
let args = qx.lang.Array.clone(path.node.arguments);
args.shift();
let methodName = peekMethodName();
if (methodName == "construct") {
path.node.callee = types.super();
path.node.arguments = args;
} else if (methodName) {
let replacement = types.memberExpression(
types.super(),
types.identifier(methodName),
false,
false
);
path.node.callee = replacement;
path.node.arguments = args;
}
}
}
}
};
}
}
});