Use vm2 for JS execution in node, and a vm polyfill for the browser. Use 2 standalone entrypoints for string-templates depending on env
This commit is contained in:
parent
29659361fe
commit
01dfef735f
|
@ -24,7 +24,8 @@
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"handlebars": "^4.7.6",
|
"handlebars": "^4.7.6",
|
||||||
"handlebars-utils": "^1.0.6",
|
"handlebars-utils": "^1.0.6",
|
||||||
"lodash": "^4.17.20"
|
"lodash": "^4.17.20",
|
||||||
|
"vm2": "^3.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^17.1.0",
|
"@rollup/plugin-commonjs": "^17.1.0",
|
||||||
|
|
|
@ -7,7 +7,15 @@ import globals from "rollup-plugin-node-globals"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
const plugins = [
|
export default [
|
||||||
|
{
|
||||||
|
input: "src/index.mjs",
|
||||||
|
output: {
|
||||||
|
sourcemap: !production,
|
||||||
|
format: "esm",
|
||||||
|
file: "./dist/bundle.mjs",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
@ -17,28 +25,6 @@ const plugins = [
|
||||||
builtins(),
|
builtins(),
|
||||||
json(),
|
json(),
|
||||||
production && terser(),
|
production && terser(),
|
||||||
]
|
],
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
input: "src/index.mjs",
|
|
||||||
output: {
|
|
||||||
sourcemap: !production,
|
|
||||||
format: "esm",
|
|
||||||
file: "./dist/bundle.mjs",
|
|
||||||
},
|
},
|
||||||
plugins,
|
|
||||||
},
|
|
||||||
// This is the valid configuration for a CommonJS bundle, but since we have
|
|
||||||
// no use for this, it's better to leave it out.
|
|
||||||
// {
|
|
||||||
// input: "src/index.cjs",
|
|
||||||
// output: {
|
|
||||||
// sourcemap: !production,
|
|
||||||
// format: "cjs",
|
|
||||||
// file: "./dist/bundle.cjs",
|
|
||||||
// exports: "named",
|
|
||||||
// },
|
|
||||||
// plugins,
|
|
||||||
// },
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
const CAPTURE_JS = new RegExp(/{{ js "(.*)" }}/)
|
const { atob } = require("../utilities")
|
||||||
const vm = require("vm")
|
|
||||||
|
// The method of executing JS scripts depends on the bundle being built.
|
||||||
|
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
||||||
|
let runJS
|
||||||
|
module.exports.setJSRunner = runner => (runJS = runner)
|
||||||
|
|
||||||
// Helper utility to strip square brackets from a value
|
// Helper utility to strip square brackets from a value
|
||||||
const removeSquareBrackets = value => {
|
const removeSquareBrackets = value => {
|
||||||
|
@ -27,23 +31,8 @@ const getContextValue = (path, context) => {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node polyfill for base64 encoding
|
|
||||||
const btoa = plainText => {
|
|
||||||
return Buffer.from(plainText, "utf-8").toString("base64")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node polyfill for base64 decoding
|
|
||||||
const atob = base64 => {
|
|
||||||
return Buffer.from(base64, "base64").toString("utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluates JS code against a certain context
|
// Evaluates JS code against a certain context
|
||||||
module.exports.processJS = (handlebars, context) => {
|
module.exports.processJS = (handlebars, context) => {
|
||||||
// Do not evaluate JS in a node environment
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return "JS bindings are not executed in a Node environment"
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wrap JS in a function and immediately invoke it.
|
// Wrap JS in a function and immediately invoke it.
|
||||||
// This is required to allow the final `return` statement to be valid.
|
// This is required to allow the final `return` statement to be valid.
|
||||||
|
@ -58,37 +47,8 @@ module.exports.processJS = (handlebars, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a sandbox with out context and run the JS
|
// Create a sandbox with out context and run the JS
|
||||||
vm.createContext(sandboxContext)
|
return runJS(js, sandboxContext)
|
||||||
return vm.runInNewContext(js, sandboxContext, { timeout: 1000 })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return "Error while executing JS"
|
return "Error while executing JS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if a HBS expression is a valid JS HBS expression
|
|
||||||
module.exports.isJSBinding = handlebars => {
|
|
||||||
return module.exports.decodeJSBinding(handlebars) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encodes a raw JS string as a JS HBS expression
|
|
||||||
module.exports.encodeJSBinding = javascript => {
|
|
||||||
return `{{ js "${btoa(javascript)}" }}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decodes a JS HBS expression to the raw JS string
|
|
||||||
module.exports.decodeJSBinding = handlebars => {
|
|
||||||
if (!handlebars || typeof handlebars !== "string") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS is only valid if it is the only HBS expression
|
|
||||||
if (!handlebars.trim().startsWith("{{ js ")) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = handlebars.match(CAPTURE_JS)
|
|
||||||
if (!match || match.length < 2) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return atob(match[1])
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,169 +1,28 @@
|
||||||
const handlebars = require("handlebars")
|
const { VM } = require("vm2")
|
||||||
const { registerAll } = require("./helpers/index")
|
const templates = require("./index.js")
|
||||||
const processors = require("./processors")
|
const { setJSRunner } = require("./helpers/javascript")
|
||||||
const { removeHandlebarsStatements } = require("./utilities")
|
|
||||||
const manifest = require("../manifest.json")
|
|
||||||
const JS = require("./helpers/javascript")
|
|
||||||
|
|
||||||
const hbsInstance = handlebars.create()
|
|
||||||
registerAll(hbsInstance)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function to check if the object is valid
|
* CJS entrypoint for rollup
|
||||||
*/
|
*/
|
||||||
function testObject(object) {
|
module.exports.isValid = templates.isValid
|
||||||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
module.exports.makePropSafe = templates.makePropSafe
|
||||||
try {
|
module.exports.getManifest = templates.getManifest
|
||||||
JSON.stringify(object)
|
module.exports.isJSBinding = templates.isJSBinding
|
||||||
} catch (err) {
|
module.exports.encodeJSBinding = templates.encodeJSBinding
|
||||||
throw "Unable to process inputs to JSON, cannot recurse"
|
module.exports.decodeJSBinding = templates.decodeJSBinding
|
||||||
}
|
module.exports.processStringSync = templates.processStringSync
|
||||||
}
|
module.exports.processObjectSync = templates.processObjectSync
|
||||||
|
module.exports.processString = templates.processString
|
||||||
|
module.exports.processObject = templates.processObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an input object this will recurse through all props to try and update any handlebars statements within.
|
* Use vm2 to run JS scripts in a node env
|
||||||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
|
||||||
* if the structure contains any cycles then this will fail.
|
|
||||||
* @param {object} context The context that handlebars should fill data from.
|
|
||||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
|
||||||
*/
|
*/
|
||||||
module.exports.processObject = async (object, context) => {
|
setJSRunner((js, context) => {
|
||||||
testObject(object)
|
const vm = new VM({
|
||||||
for (let key of Object.keys(object || {})) {
|
sandbox: context,
|
||||||
if (object[key] != null) {
|
timeout: 1000
|
||||||
let val = object[key]
|
|
||||||
if (typeof val === "string") {
|
|
||||||
object[key] = await module.exports.processString(object[key], context)
|
|
||||||
} else if (typeof val === "object") {
|
|
||||||
object[key] = await module.exports.processObject(object[key], context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
|
||||||
* then nothing will occur.
|
|
||||||
* @param {string} string The template string which is the filled from the context object.
|
|
||||||
* @param {object} context An object of information which will be used to enrich the string.
|
|
||||||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
|
||||||
*/
|
|
||||||
module.exports.processString = async (string, context) => {
|
|
||||||
// TODO: carry out any async calls before carrying out async call
|
|
||||||
return module.exports.processStringSync(string, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
|
|
||||||
* a pure sync call and therefore does not have the full functionality of the async call.
|
|
||||||
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
|
||||||
* if the structure contains any cycles then this will fail.
|
|
||||||
* @param {object} context The context that handlebars should fill data from.
|
|
||||||
* @returns {object|array} The structure input, as fully updated as possible.
|
|
||||||
*/
|
|
||||||
module.exports.processObjectSync = (object, context) => {
|
|
||||||
testObject(object)
|
|
||||||
for (let key of Object.keys(object || {})) {
|
|
||||||
let val = object[key]
|
|
||||||
if (typeof val === "string") {
|
|
||||||
object[key] = module.exports.processStringSync(object[key], context)
|
|
||||||
} else if (typeof val === "object") {
|
|
||||||
object[key] = module.exports.processObjectSync(object[key], context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
|
||||||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
|
||||||
* @param {string} string The template string which is the filled from the context object.
|
|
||||||
* @param {object} context An object of information which will be used to enrich the string.
|
|
||||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
|
||||||
*/
|
|
||||||
module.exports.processStringSync = (string, context) => {
|
|
||||||
if (!exports.isValid(string)) {
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
// take a copy of input incase error
|
|
||||||
const input = string
|
|
||||||
if (typeof string !== "string") {
|
|
||||||
throw "Cannot process non-string types."
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
string = processors.preprocess(string)
|
|
||||||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
|
||||||
const template = hbsInstance.compile(string, {
|
|
||||||
strict: false,
|
|
||||||
})
|
})
|
||||||
return processors.postprocess(template({
|
return vm.run(js)
|
||||||
now: new Date().toISOString(),
|
})
|
||||||
...context,
|
|
||||||
}))
|
|
||||||
} catch (err) {
|
|
||||||
return removeHandlebarsStatements(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
|
|
||||||
* @param {string} property The property which is to be wrapped.
|
|
||||||
* @returns {string} The wrapped property ready to be added to a templating string.
|
|
||||||
*/
|
|
||||||
module.exports.makePropSafe = property => {
|
|
||||||
return `[${property}]`.replace("[[", "[").replace("]]", "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
|
|
||||||
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
|
|
||||||
* @returns {boolean} Whether or not the input string is valid.
|
|
||||||
*/
|
|
||||||
module.exports.isValid = string => {
|
|
||||||
const validCases = [
|
|
||||||
"string",
|
|
||||||
"number",
|
|
||||||
"object",
|
|
||||||
"array",
|
|
||||||
"cannot read property",
|
|
||||||
"undefined",
|
|
||||||
]
|
|
||||||
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
|
||||||
const invalidCases = [`expecting '`]
|
|
||||||
// don't really need a real context to check if its valid
|
|
||||||
const context = {}
|
|
||||||
try {
|
|
||||||
hbsInstance.compile(processors.preprocess(string, false))(context)
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err && err.message ? err.message : err
|
|
||||||
if (!msg) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const invalidCase = invalidCases.some(invalidCase =>
|
|
||||||
msg.toLowerCase().includes(invalidCase)
|
|
||||||
)
|
|
||||||
const validCase = validCases.some(validCase =>
|
|
||||||
msg.toLowerCase().includes(validCase)
|
|
||||||
)
|
|
||||||
// special case for maths functions - don't have inputs yet
|
|
||||||
return validCase && !invalidCase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We have generated a static manifest file from the helpers that this string templating package makes use of.
|
|
||||||
* This manifest provides information about each of the helpers and how it can be used.
|
|
||||||
* @returns The manifest JSON which has been generated from the helpers.
|
|
||||||
*/
|
|
||||||
module.exports.getManifest = () => {
|
|
||||||
return manifest
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export utilities for working with JS bindings
|
|
||||||
*/
|
|
||||||
module.exports.isJSBinding = JS.isJSBinding
|
|
||||||
module.exports.decodeJSBinding = JS.decodeJSBinding
|
|
||||||
module.exports.encodeJSBinding = JS.encodeJSBinding
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
const handlebars = require("handlebars")
|
||||||
|
const { registerAll } = require("./helpers/index")
|
||||||
|
const processors = require("./processors")
|
||||||
|
const { removeHandlebarsStatements, atob, btoa } = require("./utilities")
|
||||||
|
const manifest = require("../manifest.json")
|
||||||
|
|
||||||
|
const hbsInstance = handlebars.create()
|
||||||
|
registerAll(hbsInstance)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* utility function to check if the object is valid
|
||||||
|
*/
|
||||||
|
function testObject(object) {
|
||||||
|
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
||||||
|
try {
|
||||||
|
JSON.stringify(object)
|
||||||
|
} catch (err) {
|
||||||
|
throw "Unable to process inputs to JSON, cannot recurse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an input object this will recurse through all props to try and update any handlebars statements within.
|
||||||
|
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||||
|
* if the structure contains any cycles then this will fail.
|
||||||
|
* @param {object} context The context that handlebars should fill data from.
|
||||||
|
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||||
|
*/
|
||||||
|
module.exports.processObject = async (object, context) => {
|
||||||
|
testObject(object)
|
||||||
|
for (let key of Object.keys(object || {})) {
|
||||||
|
if (object[key] != null) {
|
||||||
|
let val = object[key]
|
||||||
|
if (typeof val === "string") {
|
||||||
|
object[key] = await module.exports.processString(object[key], context)
|
||||||
|
} else if (typeof val === "object") {
|
||||||
|
object[key] = await module.exports.processObject(object[key], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||||
|
* then nothing will occur.
|
||||||
|
* @param {string} string The template string which is the filled from the context object.
|
||||||
|
* @param {object} context An object of information which will be used to enrich the string.
|
||||||
|
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||||
|
*/
|
||||||
|
module.exports.processString = async (string, context) => {
|
||||||
|
// TODO: carry out any async calls before carrying out async call
|
||||||
|
return module.exports.processStringSync(string, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
|
||||||
|
* a pure sync call and therefore does not have the full functionality of the async call.
|
||||||
|
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||||
|
* if the structure contains any cycles then this will fail.
|
||||||
|
* @param {object} context The context that handlebars should fill data from.
|
||||||
|
* @returns {object|array} The structure input, as fully updated as possible.
|
||||||
|
*/
|
||||||
|
module.exports.processObjectSync = (object, context) => {
|
||||||
|
testObject(object)
|
||||||
|
for (let key of Object.keys(object || {})) {
|
||||||
|
let val = object[key]
|
||||||
|
if (typeof val === "string") {
|
||||||
|
object[key] = module.exports.processStringSync(object[key], context)
|
||||||
|
} else if (typeof val === "object") {
|
||||||
|
object[key] = module.exports.processObjectSync(object[key], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||||
|
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
||||||
|
* @param {string} string The template string which is the filled from the context object.
|
||||||
|
* @param {object} context An object of information which will be used to enrich the string.
|
||||||
|
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||||
|
*/
|
||||||
|
module.exports.processStringSync = (string, context) => {
|
||||||
|
if (!exports.isValid(string)) {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
// take a copy of input incase error
|
||||||
|
const input = string
|
||||||
|
if (typeof string !== "string") {
|
||||||
|
throw "Cannot process non-string types."
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
string = processors.preprocess(string)
|
||||||
|
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||||
|
const template = hbsInstance.compile(string, {
|
||||||
|
strict: false,
|
||||||
|
})
|
||||||
|
return processors.postprocess(
|
||||||
|
template({
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
...context,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
return removeHandlebarsStatements(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
|
||||||
|
* @param {string} property The property which is to be wrapped.
|
||||||
|
* @returns {string} The wrapped property ready to be added to a templating string.
|
||||||
|
*/
|
||||||
|
module.exports.makePropSafe = property => {
|
||||||
|
return `[${property}]`.replace("[[", "[").replace("]]", "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
|
||||||
|
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
|
||||||
|
* @returns {boolean} Whether or not the input string is valid.
|
||||||
|
*/
|
||||||
|
module.exports.isValid = string => {
|
||||||
|
const validCases = [
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"cannot read property",
|
||||||
|
"undefined",
|
||||||
|
]
|
||||||
|
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
||||||
|
const invalidCases = [`expecting '`]
|
||||||
|
// don't really need a real context to check if its valid
|
||||||
|
const context = {}
|
||||||
|
try {
|
||||||
|
hbsInstance.compile(processors.preprocess(string, false))(context)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err && err.message ? err.message : err
|
||||||
|
if (!msg) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const invalidCase = invalidCases.some(invalidCase =>
|
||||||
|
msg.toLowerCase().includes(invalidCase)
|
||||||
|
)
|
||||||
|
const validCase = validCases.some(validCase =>
|
||||||
|
msg.toLowerCase().includes(validCase)
|
||||||
|
)
|
||||||
|
// special case for maths functions - don't have inputs yet
|
||||||
|
return validCase && !invalidCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have generated a static manifest file from the helpers that this string templating package makes use of.
|
||||||
|
* This manifest provides information about each of the helpers and how it can be used.
|
||||||
|
* @returns The manifest JSON which has been generated from the helpers.
|
||||||
|
*/
|
||||||
|
module.exports.getManifest = () => {
|
||||||
|
return manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a HBS expression is a valid JS HBS expression
|
||||||
|
* @param handlebars the HBS expression to check
|
||||||
|
* @returns {boolean} whether the expression is JS or not
|
||||||
|
*/
|
||||||
|
module.exports.isJSBinding = handlebars => {
|
||||||
|
return module.exports.decodeJSBinding(handlebars) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a raw JS string as a JS HBS expression
|
||||||
|
* @param javascript the JS code to encode
|
||||||
|
* @returns {string} the JS HBS expression
|
||||||
|
*/
|
||||||
|
module.exports.encodeJSBinding = javascript => {
|
||||||
|
return `{{ js "${btoa(javascript)}" }}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a JS HBS expression to the raw JS code
|
||||||
|
* @param handlebars the JS HBS expression
|
||||||
|
* @returns {string|null} the raw JS code
|
||||||
|
*/
|
||||||
|
module.exports.decodeJSBinding = handlebars => {
|
||||||
|
if (!handlebars || typeof handlebars !== "string") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS is only valid if it is the only HBS expression
|
||||||
|
if (!handlebars.trim().startsWith("{{ js ")) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const captureJSRegex = new RegExp(/{{ js "(.*)" }}/)
|
||||||
|
const match = handlebars.match(captureJSRegex)
|
||||||
|
if (!match || match.length < 2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return atob(match[1])
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import templates from "./index.cjs"
|
import vm from "vm"
|
||||||
|
import templates from "./index.js"
|
||||||
|
import { setJSRunner } from "./helpers/javascript"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file is simply an entrypoint for rollup - makes a lot of cjs problems go away
|
* ES6 entrypoint for rollup
|
||||||
*/
|
*/
|
||||||
export const isValid = templates.isValid
|
export const isValid = templates.isValid
|
||||||
export const makePropSafe = templates.makePropSafe
|
export const makePropSafe = templates.makePropSafe
|
||||||
|
@ -13,3 +15,11 @@ export const processStringSync = templates.processStringSync
|
||||||
export const processObjectSync = templates.processObjectSync
|
export const processObjectSync = templates.processObjectSync
|
||||||
export const processString = templates.processString
|
export const processString = templates.processString
|
||||||
export const processObject = templates.processObject
|
export const processObject = templates.processObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use polyfilled vm to run JS scripts in a browser Env
|
||||||
|
*/
|
||||||
|
setJSRunner((js, context) => {
|
||||||
|
vm.createContext(context)
|
||||||
|
return vm.runInNewContext(js, context, { timeout: 1000 })
|
||||||
|
})
|
|
@ -22,3 +22,11 @@ module.exports.removeHandlebarsStatements = string => {
|
||||||
}
|
}
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.btoa = plainText => {
|
||||||
|
return Buffer.from(plainText, "utf-8").toString("base64")
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.atob = base64 => {
|
||||||
|
return Buffer.from(base64, "base64").toString("utf-8")
|
||||||
|
}
|
||||||
|
|
|
@ -4,21 +4,7 @@ const processJS = (js, context) => {
|
||||||
return processStringSync(encodeJSBinding(js), context)
|
return processStringSync(encodeJSBinding(js), context)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Test the JavaScript helper in Node", () => {
|
|
||||||
it("should not execute JS in Node", () => {
|
|
||||||
const output = processJS(`return 1`)
|
|
||||||
expect(output).toBe("JS bindings are not executed in a Node environment")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Test the JavaScript helper", () => {
|
describe("Test the JavaScript helper", () => {
|
||||||
// JS bindings do not get evaluated on the server for safety.
|
|
||||||
// Since we want to run SJ for tests, we fake a window object to make
|
|
||||||
// it think that we're in the browser
|
|
||||||
beforeEach(() => {
|
|
||||||
window = {}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should execute a simple expression", () => {
|
it("should execute a simple expression", () => {
|
||||||
const output = processJS(`return 1 + 2`)
|
const output = processJS(`return 1 + 2`)
|
||||||
expect(output).toBe("3")
|
expect(output).toBe("3")
|
||||||
|
@ -84,4 +70,16 @@ describe("Test the JavaScript helper", () => {
|
||||||
const output = processJS(`while (true) {}`)
|
const output = processJS(`while (true) {}`)
|
||||||
expect(output).toBe("Error while executing JS")
|
expect(output).toBe("Error while executing JS")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should prevent access to the process global", () => {
|
||||||
|
const output = processJS(`return process`)
|
||||||
|
expect(output).toBe("Error while executing JS")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should prevent sandbox escape", () => {
|
||||||
|
const output = processJS(
|
||||||
|
`return this.constructor.constructor("return process")()`
|
||||||
|
)
|
||||||
|
expect(output).toBe("Error while executing JS")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4572,6 +4572,11 @@ vlq@^0.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
|
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
|
||||||
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
|
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
|
||||||
|
|
||||||
|
vm2@^3.9.4:
|
||||||
|
version "3.9.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.4.tgz#2e118290fefe7bd8ea09ebe2f5faf53730dbddaa"
|
||||||
|
integrity sha512-sOdharrJ7KEePIpHekiWaY1DwgueuiBeX/ZBJUPgETsVlJsXuEx0K0/naATq2haFvJrvZnRiORQRubR0b7Ye6g==
|
||||||
|
|
||||||
w3c-hr-time@^1.0.2:
|
w3c-hr-time@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||||
|
|
Loading…
Reference in New Issue