mirror of https://github.com/Budibase/budibase.git
75 changed files with 1954 additions and 441 deletions
@ -0,0 +1,159 @@ |
|||
<script context="module"> |
|||
import { Label } from "@budibase/bbui" |
|||
|
|||
export const EditorModes = { |
|||
JS: { |
|||
name: "javascript", |
|||
json: false, |
|||
}, |
|||
JSON: { |
|||
name: "javascript", |
|||
json: true, |
|||
}, |
|||
SQL: { |
|||
name: "sql", |
|||
}, |
|||
Handlebars: { |
|||
name: "handlebars", |
|||
base: "text/html", |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<script> |
|||
import CodeMirror from "components/integration/codemirror" |
|||
import { themeStore } from "builderStore" |
|||
import { createEventDispatcher, onMount } from "svelte" |
|||
|
|||
export let mode = EditorModes.JS |
|||
export let value = "" |
|||
export let height = 300 |
|||
export let resize = "none" |
|||
export let readonly = false |
|||
export let hints = [] |
|||
export let label |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
let textarea |
|||
let editor |
|||
|
|||
// Keep editor up to date with value |
|||
$: editor?.setValue(value || "") |
|||
|
|||
// Creates an instance of a code mirror editor |
|||
async function createEditor(mode, value) { |
|||
if (!CodeMirror || !textarea || editor) { |
|||
return |
|||
} |
|||
|
|||
// Configure CM options |
|||
const lightTheme = $themeStore.theme.includes("light") |
|||
const options = { |
|||
mode, |
|||
value: value || "", |
|||
readOnly: readonly, |
|||
theme: lightTheme ? "default" : "tomorrow-night-eighties", |
|||
|
|||
// Style |
|||
lineNumbers: true, |
|||
lineWrapping: true, |
|||
indentWithTabs: true, |
|||
indentUnit: 2, |
|||
tabSize: 2, |
|||
|
|||
// QOL addons |
|||
extraKeys: { "Ctrl-Space": "autocomplete" }, |
|||
styleActiveLine: { nonEmpty: true }, |
|||
autoCloseBrackets: true, |
|||
matchBrackets: true, |
|||
} |
|||
|
|||
// Register hints plugin if desired |
|||
if (hints?.length) { |
|||
CodeMirror.registerHelper("hint", "dictionaryHint", function (editor) { |
|||
const cursor = editor.getCursor() |
|||
return { |
|||
list: hints, |
|||
from: CodeMirror.Pos(cursor.line, cursor.ch), |
|||
to: CodeMirror.Pos(cursor.line, cursor.ch), |
|||
} |
|||
}) |
|||
CodeMirror.commands.autocomplete = function (cm) { |
|||
CodeMirror.showHint(cm, CodeMirror.hint.dictionaryHint) |
|||
} |
|||
} |
|||
|
|||
// Construct CM instance |
|||
editor = CodeMirror.fromTextArea(textarea, options) |
|||
|
|||
// Use a blur handler to update the value |
|||
editor.on("blur", instance => { |
|||
dispatch("change", instance.getValue()) |
|||
}) |
|||
} |
|||
|
|||
// Export a function to expose caret position |
|||
export const getCaretPosition = () => { |
|||
const cursor = editor.getCursor() |
|||
return { |
|||
start: cursor.ch, |
|||
end: cursor.ch, |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
// Create the editor with initial value |
|||
createEditor(mode, value) |
|||
|
|||
// Clean up editor on unmount |
|||
return () => { |
|||
if (editor) { |
|||
editor.toTextArea() |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
{#if label} |
|||
<div style="margin-bottom: var(--spacing-s)"> |
|||
<Label small>{label}</Label> |
|||
</div> |
|||
{/if} |
|||
<div |
|||
style={`--code-mirror-height: ${height}px; --code-mirror-resize: ${resize}`} |
|||
> |
|||
<textarea tabindex="0" bind:this={textarea} readonly {value} /> |
|||
</div> |
|||
|
|||
<style> |
|||
div :global(.CodeMirror) { |
|||
height: var(--code-mirror-height); |
|||
min-height: var(--code-mirror-height); |
|||
font-family: monospace; |
|||
line-height: 1.3; |
|||
border: var(--spectrum-alias-border-size-thin) solid; |
|||
border-color: var(--spectrum-alias-border-color); |
|||
border-radius: var(--border-radius-s); |
|||
resize: var(--code-mirror-resize); |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* Override default active line highlight colour in dark theme */ |
|||
div |
|||
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties |
|||
.CodeMirror-activeline-background) { |
|||
background: rgba(255, 255, 255, 0.075); |
|||
} |
|||
|
|||
/* Remove active line styling when not focused */ |
|||
div |
|||
:global(.CodeMirror:not(.CodeMirror-focused) |
|||
.CodeMirror-activeline-background) { |
|||
background: unset; |
|||
} |
|||
|
|||
/* Add a spectrum themed border when focused */ |
|||
div :global(.CodeMirror-focused) { |
|||
border-color: var(--spectrum-alias-border-color-mouse-focus); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { Input } from "@budibase/bbui" |
|||
import { isJSBinding } from "@budibase/string-templates" |
|||
|
|||
export let value |
|||
|
|||
$: isJS = isJSBinding(value) |
|||
</script> |
|||
|
|||
<Input |
|||
{...$$props} |
|||
value={isJS ? "(JavaScript function)" : value} |
|||
readonly={isJS} |
|||
on:change |
|||
/> |
|||
@ -1,12 +1,22 @@ |
|||
import CodeMirror from "codemirror" |
|||
import "codemirror/lib/codemirror.css" |
|||
import "codemirror/theme/tomorrow-night-eighties.css" |
|||
import "codemirror/addon/hint/show-hint.css" |
|||
import "codemirror/theme/neo.css" |
|||
|
|||
// Modes
|
|||
import "codemirror/mode/javascript/javascript" |
|||
import "codemirror/mode/sql/sql" |
|||
import "codemirror/mode/css/css" |
|||
import "codemirror/mode/handlebars/handlebars" |
|||
import "codemirror/mode/javascript/javascript" |
|||
|
|||
// Hints
|
|||
import "codemirror/addon/hint/show-hint" |
|||
import "codemirror/addon/hint/show-hint.css" |
|||
|
|||
// Theming
|
|||
import "codemirror/theme/tomorrow-night-eighties.css" |
|||
|
|||
// Functional addons
|
|||
import "codemirror/addon/selection/active-line" |
|||
import "codemirror/addon/edit/closebrackets" |
|||
import "codemirror/addon/edit/matchbrackets" |
|||
|
|||
export default CodeMirror |
|||
|
|||
@ -0,0 +1,21 @@ |
|||
const fetch = require("node-fetch") |
|||
const { VM, VMScript } = require("vm2") |
|||
|
|||
class ScriptRunner { |
|||
constructor(script, context) { |
|||
const code = `let fn = () => {\n${script}\n}; results.out = fn();` |
|||
this.vm = new VM() |
|||
this.results = { out: "" } |
|||
this.vm.setGlobals(context) |
|||
this.vm.setGlobal("fetch", fetch) |
|||
this.vm.setGlobal("results", this.results) |
|||
this.script = new VMScript(code) |
|||
} |
|||
|
|||
execute() { |
|||
this.vm.run(this.script) |
|||
return this.results.out |
|||
} |
|||
} |
|||
|
|||
module.exports = ScriptRunner |
|||
File diff suppressed because it is too large
@ -0,0 +1,49 @@ |
|||
const { atob } = require("../utilities") |
|||
|
|||
// 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
|
|||
const removeSquareBrackets = value => { |
|||
if (!value || typeof value !== "string") { |
|||
return value |
|||
} |
|||
const regex = /\[+(.+)]+/ |
|||
const matches = value.match(regex) |
|||
if (matches && matches[1]) { |
|||
return matches[1] |
|||
} |
|||
return value |
|||
} |
|||
|
|||
// Our context getter function provided to JS code as $.
|
|||
// Extracts a value from context.
|
|||
const getContextValue = (path, context) => { |
|||
let data = context |
|||
path.split(".").forEach(key => { |
|||
if (data == null || typeof data !== "object") { |
|||
return null |
|||
} |
|||
data = data[removeSquareBrackets(key)] |
|||
}) |
|||
return data |
|||
} |
|||
|
|||
// Evaluates JS code against a certain context
|
|||
module.exports.processJS = (handlebars, context) => { |
|||
try { |
|||
// Wrap JS in a function and immediately invoke it.
|
|||
// This is required to allow the final `return` statement to be valid.
|
|||
const js = `function run(){${atob(handlebars)}};run();` |
|||
|
|||
// Our $ context function gets a value from context
|
|||
const sandboxContext = { $: path => getContextValue(path, context) } |
|||
|
|||
// Create a sandbox with out context and run the JS
|
|||
return runJS(js, sandboxContext) |
|||
} catch (error) { |
|||
return "Error while executing JS" |
|||
} |
|||
} |
|||
@ -1,161 +1,28 @@ |
|||
const handlebars = require("handlebars") |
|||
const { registerAll } = require("./helpers/index") |
|||
const processors = require("./processors") |
|||
const { removeHandlebarsStatements } = 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 |
|||
} |
|||
const { VM } = require("vm2") |
|||
const templates = require("./index.js") |
|||
const { setJSRunner } = require("./helpers/javascript") |
|||
|
|||
/** |
|||
* CJS entrypoint for rollup |
|||
*/ |
|||
module.exports.isValid = templates.isValid |
|||
module.exports.makePropSafe = templates.makePropSafe |
|||
module.exports.getManifest = templates.getManifest |
|||
module.exports.isJSBinding = templates.isJSBinding |
|||
module.exports.encodeJSBinding = templates.encodeJSBinding |
|||
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 |
|||
|
|||
/** |
|||
* Use vm2 to run JS scripts in a node env |
|||
*/ |
|||
setJSRunner((js, context) => { |
|||
const vm = new VM({ |
|||
sandbox: context, |
|||
timeout: 1000 |
|||
}) |
|||
return vm.run(js) |
|||
}) |
|||
@ -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,12 +1,31 @@ |
|||
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 makePropSafe = templates.makePropSafe |
|||
export const getManifest = templates.getManifest |
|||
export const isJSBinding = templates.isJSBinding |
|||
export const encodeJSBinding = templates.encodeJSBinding |
|||
export const decodeJSBinding = templates.decodeJSBinding |
|||
export const processStringSync = templates.processStringSync |
|||
export const processObjectSync = templates.processObjectSync |
|||
export const processString = templates.processString |
|||
export const processObject = templates.processObject |
|||
|
|||
/** |
|||
* Use polyfilled vm to run JS scripts in a browser Env |
|||
*/ |
|||
setJSRunner((js, context) => { |
|||
context = { |
|||
...context, |
|||
alert: undefined, |
|||
setInterval: undefined, |
|||
setTimeout: undefined, |
|||
} |
|||
vm.createContext(context) |
|||
return vm.runInNewContext(js, context, { timeout: 1000 }) |
|||
}) |
|||
@ -0,0 +1,85 @@ |
|||
const { processStringSync, encodeJSBinding } = require("../src/index.cjs") |
|||
|
|||
const processJS = (js, context) => { |
|||
return processStringSync(encodeJSBinding(js), context) |
|||
} |
|||
|
|||
describe("Test the JavaScript helper", () => { |
|||
it("should execute a simple expression", () => { |
|||
const output = processJS(`return 1 + 2`) |
|||
expect(output).toBe("3") |
|||
}) |
|||
|
|||
it("should be able to use primitive bindings", () => { |
|||
const output = processJS(`return $("foo")`, { |
|||
foo: "bar", |
|||
}) |
|||
expect(output).toBe("bar") |
|||
}) |
|||
|
|||
it("should be able to use an object binding", () => { |
|||
const output = processJS(`return $("foo").bar`, { |
|||
foo: { |
|||
bar: "baz", |
|||
}, |
|||
}) |
|||
expect(output).toBe("baz") |
|||
}) |
|||
|
|||
it("should be able to use a complex object binding", () => { |
|||
const output = processJS(`return $("foo").bar[0].baz`, { |
|||
foo: { |
|||
bar: [ |
|||
{ |
|||
baz: "shazbat", |
|||
}, |
|||
], |
|||
}, |
|||
}) |
|||
expect(output).toBe("shazbat") |
|||
}) |
|||
|
|||
it("should be able to use a deep binding", () => { |
|||
const output = processJS(`return $("foo.bar.baz")`, { |
|||
foo: { |
|||
bar: { |
|||
baz: "shazbat", |
|||
}, |
|||
}, |
|||
}) |
|||
expect(output).toBe("shazbat") |
|||
}) |
|||
|
|||
it("should be able to use a deep array binding", () => { |
|||
const output = processJS(`return $("foo.0.bar")`, { |
|||
foo: [ |
|||
{ |
|||
bar: "baz", |
|||
}, |
|||
], |
|||
}) |
|||
expect(output).toBe("baz") |
|||
}) |
|||
|
|||
it("should handle errors", () => { |
|||
const output = processJS(`throw "Error"`) |
|||
expect(output).toBe("Error while executing JS") |
|||
}) |
|||
|
|||
it("should timeout after one second", () => { |
|||
const output = processJS(`while (true) {}`) |
|||
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") |
|||
}) |
|||
}) |
|||
Loading…
Reference in new issue