All files / lib/pipeline/module evalEngine.ts

94.59% Statements 35/37
93.75% Branches 15/16
100% Functions 7/7
94.59% Lines 35/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146                  27x   159x   158x   80x       78x             27x         27x             27x   1529x 85x       1444x 67x       1377x                     27x   100x   54x       46x 46x   44x     44x     2x                   27x   54x     54x                   27x   44x     44x                       27x 111x       111x                   27x 2x 1x 1x           2x  
/**
 * Invoke an EvalFunc in the specified EvalContext.
 *
 * @param evalFunction Function to invoke
 * @param evalContext Context to execute within
 * @returns The return value of evalFunction
 */
import {EvalContext, EvalFunction} from '../..';
 
export async function invokeEvalFunc<T>(evalFunction: EvalFunction<T>, evalContext: EvalContext): Promise<T> {
    // execute the function
    const returnOrPromise = evalFunction.call(evalContext.scope, evalContext.scope, evalContext, requireFromRoot);
 
    if (returnOrPromise instanceof Promise) {
        // If its a promise, await it
        return await returnOrPromise;
 
    } else {
        // If not a promise, then return directly
        return returnOrPromise;
    }
}
 
/**
 * regular expression to detect a JS template string literal
 */
const templateTextRegex = /(?<!\\)\${(([^\\}]|\\}|\\)*)}/;
 
/**
 * regular expression to detect handlebars {{ }}
 */
const handlebarsRegex = /^\s*(?<!\\){{(.*)}}\s*$/;
 
/**
 * Check if the given string contains embedded JS script(s) that should be executed.
 * @param expression The string to check
 * @returns true if the string contains any recognized expressions
 */
export function isExpressionString(expression: string): boolean {
    // value is template string
    if (templateTextRegex.test(expression)) {
        return true;
    }
 
    // value is handlebars
    if (handlebarsRegex.test(expression)) {
        return true;
    }
 
    // value is plain text
    return false;
}
 
/**
 * Compiles JS code embedded within a string, and then returns a callable function that will return the output of that code.
 * Result object is stateless and can be safely cached and reused.
 *
 * @param expression The string to compile.
 * @returns an EvalFunction that will return the result of the expression
 * @throws if the provided string contains no expressions
 */
export function parseExpression(expression: string): EvalFunction<unknown> {
    // value is template string
    if (templateTextRegex.test(expression)) {
        // parse into function
        return parseTemplateString(expression);
    }
 
    // value is handlebars
    const handlebarsMatches: RegExpMatchArray | null = handlebarsRegex.exec(expression);
    if (handlebarsMatches != null && handlebarsMatches.length === 2) {
        // get JS code from handlebars text
        const handlebarCode: string = handlebarsMatches[1];
 
        // parse into function
        return parseHandlebars(handlebarCode);
    }
 
    throw new Error('Attempting to compile plain text as JavaScript');
}
 
/**
 * Parse an ES6 template literal
 *
 * @param templateString Contents of the template string, excluding the backticks
 * @returns EvalFunction that will execute the template string and return a standard string
 * @throws If the template literal cannot be parsed
 */
export function parseTemplateString(templateString: string): EvalFunction<string> {
    // generate function body for template
    const functionBody = `return $$.expressionTagger\`${ templateString }\`;`;
 
    // create content
    return parseScript(functionBody);
}
 
/**
 * Parse a handlebars expression.  Ex. {{ foo() }}
 *
 * @param jsString Contents of the handlebars expression, excluding the braces
 * @returns EvalFunction that will execute the expression and return the resulting object.
 * @throws If the script code cannot be parsed
 */
export function parseHandlebars(jsString: string): EvalFunction<unknown> {
    // generate body for function
    const functionBody = `return ${ jsString };`;
 
    // create content
    return parseScript(functionBody);
}
 
/**
 * Parse arbitrary JS code in a function context.
 * All JS features are available, provided that they are valid for use within a function body.
 * The function can optionally return a value, but return values are not type checked.
 *
 * @param functionBody JS code to execute
 * @returns EvalFunction that will execute the expression and return the result of the function, if any.
 * @throws If the JS code cannot be parsed
 */
export function parseScript<T>(functionBody: string): EvalFunction<T> {
    try {
        // Parse function body into callable function.
        // This is inherently not type-safe, as the purpose is to run unknown JS code.
        // eslint-disable-next-line @typescript-eslint/no-implied-eval
        return new Function('$', '$$', 'require', functionBody) as EvalFunction<T>;
    } catch (error) {
        throw new Error(`Parse error in function: ${ error }. Function body: ${ functionBody }`);
    }
}
 
/**
 * Calls node.js require() relatively from the Mooltipage root
 * @param path Path to require
 */
export function requireFromRoot(path: string): unknown {
    if (path.startsWith('./')) {
        path = `../../${ path }`;
    } else Iif (path.startsWith('.\\')) {
        path = `..\\..\\${ path }`;
    }
 
    // explicit use of require() is necessary here
    // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
    return require(path) as unknown;
}