@jeli/compiler-cli
Version:
jeli compiler for frontend development
475 lines (428 loc) • 16.8 kB
JavaScript
const helper = require('@jeli/cli/lib/utils');
const { escogen, parseAst } = require('./ast.generator');
const { parseQuery } = require('./query_selector');
const loader = require('./loader');
const AnnotationsEnum = {
SERVICES: 'SERVICES',
REQUIREDMODULES: 'REQUIREDMODULES',
SELECTORS: 'SELECTORS',
EXPORTS: 'EXPORTS',
ROOTELEMENT: 'ROOTELEMENT'
};
/**
*
* @param {*} ast
* @param {*} filePath
* @param {*} outputInstance
* @param {*} componentsResolver
*/
function processAst(ast, filePath, outputInstance, componentsResolver) {
const annotations = [escogen(ast.impl)];
const compilerError = [];
const fn = ast.impl[0].id.name;
try {
_structureDI(ast.definitions);
_validateDI(ast.definitions.DI, fn, fn);
switch (ast.type.toLowerCase()) {
case ('directive'):
case ('element'):
_registerOrThrowError(ast.type, fn, ast.definitions,
`Class ${helper.colors.yellow(fn)} is already registered, please use a prefix to differentiate them.`);
elementParser(ast.definitions, ast.type, fn);
break;
case ('service'):
case ('provider'):
case ('pipe'):
_registerOrThrowError('Service', fn, ast.definitions,
`Service ${helper.colors.yellow(fn)} is already registered, please rename Class or use a prefix`);
break;
case ('jmodule'):
_registerOrThrowError(ast.type, fn, ast.definitions, `Modules ${helper.colors.yellow(fn)} is already registered.`);
validateModule(ast.definitions, fn);
break;
}
} catch (e) {
console.log(e)
}
if (compilerError.length) {
loader.spinner.fail(`Errors found in: ${helper.colors.yellow(filePath)}\n`)
helper.console.error(compilerError.join('\n'));
} else {
outputInstance.annotations.push({
fn,
type: ast.type,
isModule: helper.is(ast.type, 'jModule'),
annotations
})
}
/**
*
* @param {*} moduleObj
* @param {*} fnName
*/
function validateModule(moduleObj, fnName) {
Object.keys(moduleObj).forEach(_validate);
/**
*
* @param {*} type
*/
function _validate(type) {
switch (type.toUpperCase()) {
case (AnnotationsEnum.SERVICES):
_validateService();
break;
case (AnnotationsEnum.REQUIREDMODULES):
_validateRequiredModules();
break;
case (AnnotationsEnum.SELECTORS):
_validateSelectors();
break;
case (AnnotationsEnum.EXPORTS):
_validateExports();
break;
case (AnnotationsEnum.ROOTELEMENT):
_validateRootElement();
break;
default:
loader.spinner.fail(`\nunsupported Module definition<${helper.colors.yellow(type)}> defined in ${helper.colors.yellow(filePath)}`);
helper.abort();
break;
}
}
/**
* Module Service Validator
*/
function _validateService() {
moduleObj.services = moduleObj.services.filter(service => {
const serviceDefinition = componentsResolver.getLocalService(service);
if (helper.typeOf(service, 'object')) {
validateToken(service);
annotations.push(`${service.name}.register(${getTokenValue(service)}, true);`);
return false;
} else if (!serviceDefinition) {
compilerError.push(`service -> ${service} implementation not found.`);
return false;
}
if (!serviceDefinition.module) {
serviceDefinition.module = fnName;
}
return true;
});
}
/**
* validate requiredModules
*/
function _validateRequiredModules() {
moduleObj.requiredModules = moduleObj.requiredModules.map(reqModule => {
if (helper.typeOf(reqModule, 'object')) {
const moduleDef = reqModule;
reqModule = reqModule.namespaces.shift();
annotations.push(`/** initialize ${reqModule} static call **/ ${reqModule}${moduleDef.namespaces.join('.')}.${moduleDef.fn}.apply(null, ${helper.objectStringToAsIs(moduleDef.args)});`);
}
const module = componentsResolver.getExportedModule(reqModule);
if (!module) {
compilerError.push(`required module {${helper.colors.yellow(reqModule)}} -> {${helper.colors.yellow(fnName)}} was not found, please import the module. \n help: "${helper.colors.green("import {" +reqModule+"} from 'REQUIRED_PATH';")}"\n`);
}
/**
* check for circular requiredModules
*/
if (module && module.requiredModules && module.requiredModules.includes(fnName)) {
compilerError.push(`Circular referrence found: ${helper.colors.yellow(reqModule)} -> ${helper.colors.yellow(fnName)} -> ${helper.colors.yellow(reqModule)}`);
}
return reqModule;
});
}
/**
* validate selectors
*/
function _validateSelectors() {
moduleObj.selectors.forEach(elementFn => {
const element = componentsResolver.getLocalSelector(elementFn);
if (helper.typeOf(elementFn, 'object')) {
if (elementFn.useExisting && elementFn.selector) {
if (!element) {
return compilerError.push(`${elementFn.useExisting} is registered in ${fnName} module but implementation does not exists.`);
}
// create a new reactive class from build
const newClassName = helper.pascalCase(elementFn.selector);
elementFn.selector = helper.removeDoubleQuote(elementFn.selector);
const newInstance = Object.assign(elementFn, {
module: fnName,
link: element
});
componentsResolver.addEntry(elementFn.selector.includes('-') ? 'Element' : 'Directive', newClassName, newInstance)
return;
}
return compilerError.push(`Invalid definition ${JSON.stringify(elementFn)}, missing property "useExisiting|selector"`);
}
if (!element) {
compilerError.push(`${elementFn} is registered in ${fnName} module but implementation does not exists.`);
return;
}
if (element.module && element.module !== fnName) {
compilerError.push(`${elementFn} is registered to ${element.module} and ${fnName} modules`);
}
element.module = fnName;
});
}
/**
* validate the rootElement
*/
function _validateRootElement() {
}
/**
* validate exported elements
*/
function _validateExports() {
}
}
/**
*
* @param {*} token
*/
function validateToken(token) {
if (!componentsResolver.isExportedToken(token.name))
compilerError.push(`Token<${token.name}> definition not found.`);
if (token.useClass) {
const serviceImpl = componentsResolver.getLocalService(token.useClass);
if (!serviceImpl) {
compilerError.push(`Missing implementation for ${token.useClass} provided in ${token.name}`);
} else if (serviceImpl.DI) {
_validateDI(serviceImpl.DI, token.name, token.useClass);
}
} else if (token.factory && token.DI) {
token.DI.forEach(di => {
if (helper.typeOf(di, 'string') && !/['"]/.test(di.charAt(0)) && !componentsResolver.isExportedToken(di)) {
compilerError.push(`unable to find dependency ${helper.colors.yellow(di)}`);
}
});
}
}
/**
*
* @param {*} deps
* @param {*} tokenName
* @param {*} className
*/
function _validateDI(deps, tokenName, className, entryClass) {
if (!deps) return;
deps.forEach(di => {
if (helper.typeOf(di, 'string')) {
const service = componentsResolver.getService(di);
if (service) {
if (service.DI) {
if (service.DI.includes(tokenName))
compilerError.push(`\nFound circular dependency: ${helper.colors.yellow(tokenName)} in ${helper.colors.yellow(di)} -> ${helper.colors.yellow(className)} ${entryClass ? '-> '+ helper.colors.yellow(entryClass): ''} as ${helper.colors.yellow(tokenName)}`);
else
_validateDI(service.DI, tokenName, di, className);
}
} else if (!entryClass) {
compilerError.push(`\nservice not found: ${helper.colors.yellow(di)}`);
}
} else if (helper.typeOf(di, 'object') && !di.optional) {
compilerError.push(`unable to resolve dependency: ${helper.colors.yellow(di.name)} -> ${helper.colors.yellow(fn)}}`);
}
});
}
/**
*
* @param {*} service
*/
function getTokenValue(service) {
const token = Object.keys(service).reduce((accum, key) => {
if (key !== 'name') {
accum.push(`${key}: ${toObjectString(service[key], key == 'DI')}`);
}
return accum;
}, []);
/**
*
* @param {*} value
* @param {*} isdeps
*/
function toObjectString(value, isdeps) {
const strinifyObj = v => helper.typeOf(v, 'object') ? JSON.stringify(v) : v;
const parseDI = v => v.map(di => {
if (helper.typeOf(di, 'string') && !/['"]/.test(di.charAt(0))) {
return di;
} else {
return `{instance:${strinifyObj(di)}}`;
}
});
if (Array.isArray(value)) {
if (isdeps) return `[${parseDI(value)}]`;
return `[${d.map(v => strinifyObj(v))}]`;
}
return value;
}
return `{${token.join(', ')}}`;
}
/**
*
* @param {*} type
* @param {*} message
*/
function _registerOrThrowError(type, fn, obj, message) {
if (!componentsResolver.addEntry(type, fn, obj)) {
compilerError.push(message);
}
}
/**
*
* @param {*} obj
* @param {*} type
* @param {*} fnName
*/
function elementParser(obj, type, fnName) {
const isElement = (type !== 'Directive');
/**
* Attach the name of the Class to the Annotation
* this will be use for dictionary purpose
*/
try {
ParseProps(obj);
ParseAndValidateResolvers(obj);
if (obj.events && obj.events.length) {
obj.events = _processEventRegistry(obj.events);
}
/**
* input:type=text:model:form-field, textarea
*/
if (/[,|:=!]/g.test(obj.selector)) {
componentsResolver.addEntry('queries', obj.selector, parseQuery(obj.selector))
}
/**
* validate registerAs
*/
if (obj.registerAs && !componentsResolver.isExportedToken(obj.registerAs)) {
compilerError.push(`Token<${obj.registerAs}> definition not found.`);
}
if (isElement) {
// reference the selector as we can only have one per module
if (!obj.selector || !helper.isContain('-', obj.selector)) {
compilerError.push(`<${obj.selector}/> does not comply with HTML-Spec standard for custom elements naming.\
Which states a custom element should contain an hyphen e.g <my-element>`);
}
ParseChild(obj,['viewChild','contentChild','contentChildren']);
}
} catch (e) {
console.log(e);
}
}
/**
*
* @param {*} obj
*/
function ParseProps(obj) {
if (obj.props && obj.props.length) {
obj.props = obj.props.reduce((accum, prop) => {
const item = helper.stringToObjectNameValueMapping(prop, false);
accum[item.name] = item;
delete item.name;
return accum;
}, {});
}
}
/**
*
* @param {*} obj
* @param {*} listOfAnnotations
*/
function ParseChild(obj, listOfAnnotations) {
listOfAnnotations.forEach(prop => {
if (obj[prop] && obj[prop].length) {
// QueryList not allowed for @contentChild
obj[prop] = obj[prop].reduce((accum, item) => {
item = helper.stringToObjectNameValueMapping(item, true, true, true);
if(['contentChild'].includes(prop) && item.ql){
compilerError.push(`QueryList cannot be used with ${prop} -> ${item}`);
return accum;
} else if ('contentChildren' == prop && !item.ql){
compilerError.push(`${prop} must be used with a QueryList -> ${item}`);
return accum;
}
accum.push(item);
return accum;
}, []);
}
});
}
/**
*
* @param {*} obj
*/
function ParseAndValidateResolvers(obj) {
if (obj.resolve && obj.resolve.length) {
obj.resolve.forEach(item => {
if (helper.typeOf(item, 'string') && !componentsResolver.isExportedToken(item)) return;
else if (helper.typeOf(item, 'object')) {}
});
}
}
/**
*
* @param {*} key
*/
function _structureDI(obj) {
if (obj.DI && obj.DI.length) {
obj.DI = obj.DI.map(di => {
if (/[=:?]/.test(di)) {
const useName = helper.isContain("=", di);
const stValue = helper.stringToObjectNameValueMapping(di, useName);
if (useName) {
obj.dynamicInjectors = true;
}
stValue.tokenName = `'${stValue.name}'`;
delete stValue.name;
return stValue;
} else {
return di;
}
});
}
}
/**
*
* @param {*} registry
*/
function _processEventRegistry(registry) {
return registry.reduce((accum, reg) => {
reg = helper.stringToObjectNameValueMapping(reg, false, true);
if (reg.type && reg.value) {
reg.value = parseAst(reg.value, true);
/**
* workaroud to add quote to arguments
*/
if (Array.isArray(reg.value)) {
for (const ast of reg.value) {
if (ast.type === "'call'") {
parseToString(ast.args);
}
}
}
}
accum[`'${reg.name}'`] = reg;
delete reg.name;
return accum;
}, {});
}
function parseToString(list) {
list.forEach((arg, idx) => {
if (helper.typeOf(arg, 'string')) {
list[idx] = `'${arg}'`;
} else if (Array.isArray(arg)) {
parseToString(arg);
}
});
}
}
/**
*
* @param {*} annotations
* @param {*} filePath
* @param {*} outputInstance
* @param {*} componentsResolver
*/
module.exports = async (annotations, filePath, outputInstance, componentsResolver) => {
annotations.forEach(ast => processAst(ast, filePath, outputInstance, componentsResolver));
}