ejs
ejs copied to clipboard
Add custom function constructor option
This adds an option called functionClass, which purpose is to allow to bring your own execution runtime.
Here's a basic example of how one could integrate quickjs-emscripten module to execute code in a WASM-powered virtual machine.
// index.js
import LoadSafeEJS from "./safe-ejs.js";
const safeEJS = await LoadSafeEJS();
const res = safeEJS.render("<%- 'hello ' + world %>", {world: 'world'});
console.log(res);
safeEJS.dispose();
// safe-ejs.js
import ejs from 'ejs/ejs.js';
import { getQuickJS } from "quickjs-emscripten"
/** @param {import('quickjs-emscripten/dist').QuickJSWASMModule} QuickJS */
function SafeEJS(QuickJS){
const vm = QuickJS.newContext();
this.dispose = function(){
vm.dispose();
};
this.render = function(source, data, ejsOptions){
return ejs.render(source, data, {...ejsOptions, functionClass})
};
const functionClass = function(argNames, funcBody){
// not implemented here:
// - caching the function between calls (thus useless for ejs.compile)
// - async, include(), probably more
return (locals, escapeFn, include, rethrow) => {
// create function from source
const ctor = vm.getProp(vm.global, 'Function');
const argNamesHandle = vm.newString(argNames);
const funcBodyHandle = vm.newString(funcBody);
const newFuncRet = vm.callFunction(ctor, vm.undefined, argNamesHandle, funcBodyHandle);
ctor.dispose();
argNamesHandle.dispose();
funcBodyHandle.dispose();
const newFunc = vm.unwrapResult(newFuncRet);
// wrap user-passed data into quickjs values
const handleList = [];
const makeDisposable = handle => (handleList.unshift(handle), handle);
const localsHandle = wrapValue(locals, makeDisposable);
// wrap the rest of the arguments
const escapeFnHandle = vm.newFunction('escapeFn', strHandle => {
const str = vm.getString(strHandle);
const res = escapeFn(str);
return vm.newString(res);
});
const includeHandle = vm.undefined;
const rethrowHandle = vm.newFunction('rethrow',
(errHandle, strHandle, flnmHandle, linenoHandle) => {
const str = vm.getString(strHandle);
const flnm = vm.getString(flnmHandle);
const lineno = vm.getNumber(linenoHandle);
const errMsg = vm.getProp(errHandle, 'message');
const errName = vm.getProp(errHandle, 'name');
const errStack = vm.getProp(errHandle, 'stack');
const err = new Error();
if(errMsg !== vm.undefined) err.message = vm.getString(errMsg);
if(errName !== vm.undefined) err.name = vm.getString(errName);
if(errStack !== vm.undefined) err.stack = vm.getString(errStack);
rethrow(err, str, flnm, lineno, escapeFn);
}
);
// execute our function
const ret = vm.callFunction(
newFunc, vm.undefined, localsHandle,
escapeFnHandle, includeHandle, rethrowHandle
);
// dispose of everything to prevent memory leaks
escapeFnHandle.dispose();
rethrowHandle.dispose();
for(const handle of handleList){
handle.dispose();
}
newFunc.dispose();
// return or throw an error
const res = vm.unwrapResult(ret);
const str = vm.getString(res);
res.dispose();
return str;
};
};
const wrapValue = function(value, makeDisposable){
if(value === undefined) return vm.undefined;
if(value === null) return vm.null;
if(typeof value === "boolean") return value ? vm.true : vm.false;
if(typeof value === "number") return makeDisposable( vm.newNumber(value) );
if(Array.isArray(value)){
const arr = makeDisposable( vm.newArray() );
for(let i = (value.length - 1); i >= 0; i--)
vm.setProp(arr, i, wrapValue(value[i], makeDisposable));
return arr;
}
if(typeof value === "object"){
const obj = makeDisposable( vm.newObject() );
for(const key in value)
vm.setProp(obj, key, wrapValue(value[key], makeDisposable));
return obj;
}
if(value.toString) return makeDisposable( vm.newString(value.toString()) );
return vm.undefined;
};
}
async function LoadSafeEJS(){
return new SafeEJS(await getQuickJS());
}
export default LoadSafeEJS;
can we get this merged?
Can this get merged? I would love to use quickjs-sandboxed templates in my app
@mde pls