@jeli/compiler-cli
Version:
jeli compiler for frontend development
508 lines (474 loc) • 17.2 kB
JavaScript
const esprima = require('esprima');
const helper = require('@jeli/cli/lib/utils');
const escodegen = require('escodegen');
const comment = require('./comment');
const expressionList = 'Directive,Element,Service,Provider,Pipe,jModule'.split(',');
const ASTDeclarations = {
IMPORT: "ImportDeclaration",
EXPORT_NAMED: "ExportNamedDeclaration",
EXPORT_ALL: "ExportAllDeclaration",
EXPORT_DEFAULT: "ExportDefaultDeclaration",
CLASS: "ClassDeclaration",
VARIABLE: "VariableDeclaration",
FUNCTION: "FunctionDeclaration"
};
const ASTExpression = {
STATEMENT: 'ExpressionStatement',
CALL: "CallExpression",
ASSIGNMENT: "AssignmentExpression",
ARRAY: 'ArrayExpression',
OBJECT: 'ObjectExpression',
MEMBER: "MemberExpression",
BINARY: "BinaryExpression",
CONDITIONAL: "ConditionalExpression",
EMPTY: "EmptyStatement",
UNARY: "UnaryExpression",
NEW: "NewExpression",
THIS: "ThisExpression",
LOGICAL: "LogicalExpression",
LITERAL: "LITERAL"
};
const ASTIdentifier = 'Identifier';
const ASTDefaultSpecifier = "ImportDefaultSpecifier";
const ASTNamespaceSpecifier = "ImportNamespaceSpecifier";
const ASTLiteralType = "Literal";
function deduceSourceType(source) {
return ['export ', 'import '].some(expimp => helper.isContain(expimp, source)) ? 'module' : 'script';
}
/**
*
* @param {*} source
* @param {*} currentProcess
* @param {*} stripBanner
*/
exports.generateAstSource = (source, currentProcess, stripBanner) => {
let ast = null;
try {
ast = esprima.parse(source, {
attachComment: false,
range: false,
loc: false,
sourceType: deduceSourceType(source)
});
} catch (e) {
throw e.message;
}
const sourceOutlet = {
annotations: [],
scripts: [],
type: ast.sourceType
};
var i = 0;
for (; i < ast.body.length; i++) {
const expression = ast.body[i];
switch (expression.type) {
case (ASTDeclarations.IMPORT):
const importItem = getValueFromAst(expression);
const existsource = currentProcess.imports.find(item => item.source === importItem.source);
if (existsource) {
existsource.specifiers.push.apply(existsource.specifiers, importItem.specifiers);
} else {
currentProcess.imports.push(importItem);
}
break;
case (ASTDeclarations.EXPORT_NAMED):
case (ASTDeclarations.EXPORT_ALL):
case (ASTDeclarations.EXPORT_DEFAULT):
if (expression.declaration) {
switch (expression.declaration.type) {
case (ASTDeclarations.CLASS):
throw new Error('Class exportation not yet supported');
case (ASTDeclarations.VARIABLE):
currentProcess.exports.push({
local: expression.declaration.declarations[0].id.name,
exported: expression.declaration.declarations[0].id.name
});
sourceOutlet.scripts.push(expression.declaration);
pushDeclarations(expression.declaration.declarations[0].id.name, 'vars');
break;
case (ASTDeclarations.FUNCTION):
var name = (expression.declaration.id || { name: 'default' }).name;
currentProcess.exports.push({
local: name,
exported: helper.is(expression.type, ASTDeclarations.EXPORT_DEFAULT) ? 'default' : name
});
sourceOutlet.scripts.push(expression.declaration);
if (name !== 'default') {
pushDeclarations(name, 'fns');
}
break;
case (ASTIdentifier):
currentProcess.exports.push({
local: expression.declaration.name,
exported: 'default'
});
break;
};
} else if (expression.specifiers) {
currentProcess.exports.push.apply(currentProcess.exports,
expression.specifiers.map(item => {
return {
exported: item.exported.name,
local: item.local.name
}
}));
} else if (expression.source) {
currentProcess.imports.push({
specifiers: [],
source: expression.source.value,
asModule: expression.source.value.includes('module')
});
}
break;
case (ASTExpression.STATEMENT):
if (isAnnotationStatement(expression)) {
// found Annotations
const impl = getFunctionImpl(ast.body, i, currentProcess.exports);
const properties = expression.expression.arguments[0];
const type = expression.expression.callee.name;
const isService = ['jmodule'].includes(type.toLowerCase());
sourceOutlet.annotations.push({
impl,
type,
definitions: properties ? generateProperties(properties.properties, true, false, isService) : {}
});
i = i + impl.length;
} else {
sourceOutlet.scripts.push(expression);
}
break;
default:
sourceOutlet.scripts.push(expression);
if (helper.is(ASTDeclarations.VARIABLE, expression.type)) {
expression.declarations.map(decl => pushDeclarations(decl.id.name, 'vars'));
} else if (helper.is(ASTDeclarations.FUNCTION, expression.type)) {
pushDeclarations(expression.id.name, 'fns')
}
break;
}
}
return sourceOutlet;
/**
*
* @param {*} name
* @param {*} type
*/
function pushDeclarations(name, type) {
if (currentProcess.declarations[type].includes(name)) {
throw new Error(`${type}<${name}> already declared, cannot be re-declare`);
}
currentProcess.declarations[type].push(name);
}
}
function isAnnotationStatement(expression) {
return (expression.expression &&
expression.expression.type == ASTExpression.CALL &&
expressionList.includes(expression.expression.callee.name))
}
/**
*
* @param {*} path
* @param {*} type
*/
function validateSourcePath(path, type) {
if (helper.isContain('*', path)) {
helper.console.warn(`patterns not allowed in ${type} statement -> ${path}`);
return false;
}
return true;
}
/**
*
* @param {*} ast
* @param {*} idx
* @param {*} exports
*/
function getFunctionImpl(ast, idx, exports) {
const entryAst = ast[idx + 1];
if (helper.isContain(entryAst.type, [ASTDeclarations.EXPORT_NAMED, ASTDeclarations.EXPORT_DEFAULT]) &&
helper.is(entryAst.declaration.type, ASTDeclarations.FUNCTION)) {
exports.push({
local: entryAst.declaration.id.name,
exported: entryAst.declaration.id.name
});
} else if (!helper.is(entryAst.type, ASTDeclarations.FUNCTION)) {
throw new Error(`Annotation should be followed by a Function Declaration`);
}
const fn = (entryAst.declaration || entryAst).id.name;
const impl = [entryAst.declaration || entryAst];
for (const expression of ast.slice(idx + 2)) {
if (isAnnotationStatement(expression)) return impl;
if (_matches(expression)) impl.push(expression);
}
function _matches(expression) {
return helper.is(ASTExpression.EMPTY, expression.type) ||
(helper.is(expression.type, ASTExpression.STATEMENT) &&
helper.is(expression.expression.type, ASTExpression.ASSIGNMENT) &&
helper.is(expression.expression.left.object.name || expression.expression.left.object.object.name, fn))
}
return impl;
}
/**
*
* @param {*} declaration
*/
function getClassDeclarationFromAst(declaration) {
return {
name: declaration.id.name,
superClass: delcaration.superClass,
body: declaration.body.body
}
}
/**
*
* @param {*} expression
* @param {*} addQuote
* @param {*} scriptMode
* @param {*} asIs
* @returns
*/
function getValueFromAst(expression, addQuote, scriptMode, asIs) {
switch (expression.type) {
case (ASTExpression.ARRAY):
return expression.elements.map(item => getValueFromAst(item, addQuote, scriptMode, asIs));
case (ASTExpression.OBJECT):
const expr = generateProperties(expression.properties, scriptMode, addQuote, asIs);
return (scriptMode || asIs) ? expr : ({
type: 'obj',
expr
});
case (ASTIdentifier):
return scriptMode && addQuote ? `'${expression.name}'` : expression.name;
case (ASTExpression.MEMBER):
const value = getNameSpaceFromAst(expression, [], addQuote);
return asIs ? value.join('.') : value;
case (ASTExpression.CONDITIONAL):
return {
type: "ite",
test: getValueFromAst(expression.test),
cons: getValueFromAst(expression.consequent),
alt: getValueFromAst(expression.alternate)
};
case (ASTExpression.ASSIGNMENT):
return {
type: "asg",
left: getValueFromAst(expression.left),
right: getValueFromAst(expression.right)
};
break;
case (ASTExpression.BINARY):
case (ASTExpression.LOGICAL):
return {
type: "bin",
left: getValueFromAst(expression.left),
ops: expression.operator,
right: getValueFromAst(expression.right)
};
case (ASTExpression.CALL):
/**
* MemberExpression
* test.test.test(a,b)
*/
const namespaces = getNameSpaceFromAst(expression.callee, [], addQuote);
const item = {
type: addQuote ? "'call'" : "call",
args: getArguments(expression.arguments, false, asIs),
fn: namespaces.pop()
};
if (namespaces.length) {
item.namespaces = namespaces;
}
return item;
case (ASTExpression.UNARY):
return {
type: "una",
ops: expression.operator,
args: getValueFromAst(expression.argument, addQuote)
};
case (ASTExpression.NEW):
return {
type: "new",
fn: expression.callee.name,
args: getArguments(expression.arguments, addQuote, asIs)
};
case (ASTDeclarations.IMPORT):
const specifiers = (expression.specifiers || []);
return ({
specifiers: specifiers.map(specifier => {
return {
local: specifier.local.name,
imported: specifier.imported ? specifier.imported.name : specifier.local.name
};
}),
default: specifiers.length && helper.is(expression.specifiers[0].type, ASTDefaultSpecifier),
nameSpace: specifiers.length && helper.is(expression.specifiers[0].type, ASTNamespaceSpecifier),
source: expression.source.value
});
default:
if (!scriptMode && expression.raw != expression.value && !asIs)
return {
type: 'raw',
value: expression.value
};
else
return expression[(asIs && expression.raw && helper.typeOf(expression.value, 'string')) ? 'raw' : 'value'];
}
}
/**
*
* @param {*} args
* @param {*} addQuote
* @param {*} raw
* @returns
*/
function getArguments(args, addQuote, raw) {
return args.map(item => {
const arg = getValueFromAst(item, addQuote, false, raw);
return (helper.is(ASTExpression.ARRAY, item.type) && !raw) ? { arg } : arg;
});
}
/**
*
* @param {*} ast
* @param {*} list
* @param {*} addQuote
*/
function getNameSpaceFromAst(ast, list, addQuote) {
if (ast.object) {
/**
* check for callExpression
*/
if (helper.is(ast.object.type, ASTExpression.CALL)) {
list.push(getValueFromAst(ast.object, addQuote));
} else if (ast.object.object) {
getNameSpaceFromAst(ast.object, list, addQuote);
} else {
list.push(helper.is(ast.object.type, ASTExpression.THIS) ? '$this' : ast.object.name);
}
} else {
list.push(ast.name || ast.value);
}
if (ast.property) {
if (ast.computed) {
list.push(getNameSpaceFromAst(ast.property, [], addQuote))
} else {
list.push(ast.property.name || ast.property.value);
}
}
if (addQuote) {
return list.map(identifier => `'${identifier}'`);
}
return list;
}
/**
*
* @param {*} properties
* @param {*} scriptMode
* @param {*} addQuote
* @param {*} asIs
* @returns
*/
function generateProperties(properties, scriptMode, addQuote = false, asIs) {
return properties.reduce((accum, prop) => {
accum[prop.key.name || prop.key.value] = getValueFromAst(prop.value, addQuote, scriptMode, asIs);
return accum;
}, {});
}
/**
*
* @param {*} expression
* @param {*} addQuote
*/
exports.parseAst = (expression, addQuote) => {
return esprima.parse(expression).body.map(statement => getValueFromAst(statement.expression, addQuote));
}
/**
*
* @param {*} expression
*/
exports.parseAstJSON = (expression, addQuote) => {
const parsed = esprima.parse(`(${expression})`);
return getValueFromAst(parsed.body[0].expression, addQuote);
}
/**
*
* @param {*} syntax
*/
exports.escogen = (syntax) => {
return escodegen.generate({
type: 'Program',
body: syntax
}, {
comment: true,
format: {
indent: {
style: " "
}
}
});
};
/**
*
* @param {*} ast
*/
exports.findDeclarations = function(ast) {
var funcDecls = [];
var globalVarDecls = [];
var funcStack = [];
function visitEachAstNode(root, enter, leave) {
function visit(node) {
function isSubNode(key) {
var child = node[key];
if (child === null) return false;
var ty = typeof child;
if (ty !== 'object') return false;
if (child.constructor === Array) return (key !== 'range');
if (key === 'loc') return false;
if ('type' in child) {
if (child.type in esprima.Syntax) return true;
debugger;
throw new Error('unexpected');
} else { return false; }
}
enter(node);
var keys = Object.keys(node);
var subNodeKeys = keys.filter(isSubNode);
for (var i = 0; i < subNodeKeys.length; i++) {
var key = subNodeKeys[i];
visit(node[key]);
}
leave(node);
}
visit(root);
}
function myEnter(node) {
if (node.type === 'FunctionDeclaration') {
var current = {
name: node.id.name,
params: node.params.map(function(p) { return p.name; }),
variables: []
}
funcDecls.push(current);
funcStack.push(current);
}
if (node.type === 'VariableDeclaration') {
var foundVarNames = node.declarations.map(function(d) { return d.id.name; });
if (funcStack.length === 0) {
globalVarDecls = globalVarDecls.concat(foundVarNames);
} else {
var onTopOfStack = funcStack[funcStack.length - 1];
onTopOfStack.variables = onTopOfStack.variables.concat(foundVarNames);
}
}
}
function myLeave(node) {
if (node.type === 'FunctionDeclaration') {
funcStack.pop();
}
}
visitEachAstNode(ast, myEnter, myLeave);
return {
vars: globalVarDecls,
funcs: funcDecls
};
}