Add working PoC of snippets for both polyfilled vm and isolated-vm
This commit is contained in:
parent
c138cbeba2
commit
b7b1e95eb8
|
@ -13,9 +13,10 @@
|
||||||
"build": "node ./scripts/build.js",
|
"build": "node ./scripts/build.js",
|
||||||
"postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && copyfiles -f ../../yarn.lock ./dist/",
|
"postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && copyfiles -f ../../yarn.lock ./dist/",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||||
|
"build:isolated-vm-lib:snippets": "esbuild --minify --bundle src/jsRunner/bundles/snippets.ts --outfile=src/jsRunner/bundles/snippets.ivm.bundle.js --platform=node --format=iife --global-name=snippets",
|
||||||
"build:isolated-vm-lib:string-templates": "esbuild --minify --bundle src/jsRunner/bundles/index-helpers.ts --outfile=src/jsRunner/bundles/index-helpers.ivm.bundle.js --platform=node --format=iife --external:handlebars --global-name=helpers",
|
"build:isolated-vm-lib:string-templates": "esbuild --minify --bundle src/jsRunner/bundles/index-helpers.ts --outfile=src/jsRunner/bundles/index-helpers.ivm.bundle.js --platform=node --format=iife --external:handlebars --global-name=helpers",
|
||||||
"build:isolated-vm-lib:bson": "esbuild --minify --bundle src/jsRunner/bundles/bsonPackage.ts --outfile=src/jsRunner/bundles/bson.ivm.bundle.js --platform=node --format=iife --global-name=bson",
|
"build:isolated-vm-lib:bson": "esbuild --minify --bundle src/jsRunner/bundles/bsonPackage.ts --outfile=src/jsRunner/bundles/bson.ivm.bundle.js --platform=node --format=iife --global-name=bson",
|
||||||
"build:isolated-vm-libs": "yarn build:isolated-vm-lib:string-templates && yarn build:isolated-vm-lib:bson",
|
"build:isolated-vm-libs": "yarn build:isolated-vm-lib:string-templates && yarn build:isolated-vm-lib:bson && yarn build:isolated-vm-lib:snippets",
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
||||||
"jest": "NODE_OPTIONS=\"--no-node-snapshot $NODE_OPTIONS\" jest",
|
"jest": "NODE_OPTIONS=\"--no-node-snapshot $NODE_OPTIONS\" jest",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Ctx } from "@budibase/types"
|
import { Ctx } from "@budibase/types"
|
||||||
import { IsolatedVM } from "../../jsRunner/vm"
|
import { IsolatedVM } from "../../jsRunner/vm"
|
||||||
import { iifeWrapper } from "../../jsRunner/utilities"
|
import { iifeWrapper } from "@budibase/string-templates"
|
||||||
|
|
||||||
export async function execute(ctx: Ctx) {
|
export async function execute(ctx: Ctx) {
|
||||||
const { script, context } = ctx.request.body
|
const { script, context } = ctx.request.body
|
||||||
|
|
|
@ -5,11 +5,13 @@ import fs from "fs"
|
||||||
export const enum BundleType {
|
export const enum BundleType {
|
||||||
HELPERS = "helpers",
|
HELPERS = "helpers",
|
||||||
BSON = "bson",
|
BSON = "bson",
|
||||||
|
SNIPPETS = "snippets",
|
||||||
}
|
}
|
||||||
|
|
||||||
const bundleSourceFile: Record<BundleType, string> = {
|
const bundleSourceFile: Record<BundleType, string> = {
|
||||||
[BundleType.HELPERS]: "./index-helpers.ivm.bundle.js",
|
[BundleType.HELPERS]: "./index-helpers.ivm.bundle.js",
|
||||||
[BundleType.BSON]: "./bson.ivm.bundle.js",
|
[BundleType.BSON]: "./bson.ivm.bundle.js",
|
||||||
|
[BundleType.SNIPPETS]: "./snippets.ivm.bundle.js",
|
||||||
}
|
}
|
||||||
const bundleSourceCode: Partial<Record<BundleType, string>> = {}
|
const bundleSourceCode: Partial<Record<BundleType, string>> = {}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
"use strict";var snippets=(()=>{var s=Object.create;var p=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var l=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var W=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),d=(e,r)=>{for(var n in r)p(e,n,{get:r[n],enumerable:!0})},o=(e,r,n,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of c(r))!m.call(e,t)&&t!==n&&p(e,t,{get:()=>r[t],enumerable:!(i=x(r,t))||i.enumerable});return e};var g=(e,r,n)=>(n=e!=null?s(l(e)):{},o(r||!e||!e.__esModule?p(n,"default",{value:e,enumerable:!0}):n,e)),v=e=>o(p({},"__esModule",{value:!0}),e);var u=W((_,f)=>{f.exports.iifeWrapper=e=>`(function(){
|
||||||
|
${e}
|
||||||
|
})();`});var y={};d(y,{default:()=>w});var a=g(u()),w=new Proxy({},{get:function(e,r){return[eval][0]((0,a.iifeWrapper)($("snippets")[r]))}});return v(y);})();
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line local-rules/no-budibase-imports
|
||||||
|
import { iifeWrapper } from "@budibase/string-templates/iife"
|
||||||
|
|
||||||
|
export default new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: function (_, name) {
|
||||||
|
// Get snippet definitions from global context, get the correct snippet
|
||||||
|
// then eval the JS.
|
||||||
|
// https://esbuild.github.io/content-types/#direct-eval for info on why
|
||||||
|
// eval is being called this way.
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
return [eval][0](iifeWrapper($("snippets")[name]))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
|
@ -21,13 +21,15 @@ export function init() {
|
||||||
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
|
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
|
||||||
invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS,
|
invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS,
|
||||||
isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS,
|
isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS,
|
||||||
}).withHelpers()
|
})
|
||||||
|
.withHelpers()
|
||||||
|
.withSnippets()
|
||||||
|
|
||||||
if (bbCtx) {
|
if (bbCtx) {
|
||||||
// If we have a context, we want to persist it to reuse the isolate
|
// If we have a context, we want to persist it to reuse the isolate
|
||||||
bbCtx.vm = vm
|
bbCtx.vm = vm
|
||||||
}
|
}
|
||||||
const { helpers, ...rest } = ctx
|
const { helpers, snippets, ...rest } = ctx
|
||||||
return vm.withContext(rest, () => vm.execute(js))
|
return vm.withContext(rest, () => vm.execute(js))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message === "Script execution timed out.") {
|
if (error.message === "Script execution timed out.") {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { IsolatedVM } from "../vm"
|
import { IsolatedVM } from "../vm"
|
||||||
import { iifeWrapper } from "../utilities"
|
import { iifeWrapper } from "@budibase/string-templates"
|
||||||
|
|
||||||
function runJSWithIsolatedVM(script: string, context: Record<string, any>) {
|
function runJSWithIsolatedVM(script: string, context: Record<string, any>) {
|
||||||
const runner = new IsolatedVM()
|
const runner = new IsolatedVM()
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export function iifeWrapper(script: string) {
|
|
||||||
return `(function(){\n${script}\n})();`
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@ import querystring from "querystring"
|
||||||
|
|
||||||
import { BundleType, loadBundle } from "../bundles"
|
import { BundleType, loadBundle } from "../bundles"
|
||||||
import { VM } from "@budibase/types"
|
import { VM } from "@budibase/types"
|
||||||
import { iifeWrapper } from "../utilities"
|
import { iifeWrapper } from "@budibase/string-templates"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
|
||||||
class ExecutionTimeoutError extends Error {
|
class ExecutionTimeoutError extends Error {
|
||||||
|
@ -98,6 +98,18 @@ export class IsolatedVM implements VM {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withSnippets() {
|
||||||
|
const snippetsSource = loadBundle(BundleType.SNIPPETS)
|
||||||
|
const script = this.isolate.compileScriptSync(
|
||||||
|
`${snippetsSource};snippets=snippets.default;`
|
||||||
|
)
|
||||||
|
script.runSync(this.vm, { timeout: this.invocationTimeout, release: false })
|
||||||
|
new Promise(() => {
|
||||||
|
script.release()
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
withContext<T>(context: Record<string, any>, executeWithContext: () => T) {
|
withContext<T>(context: Record<string, any>, executeWithContext: () => T) {
|
||||||
this.addToContext(context)
|
this.addToContext(context)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
QueryResponse,
|
QueryResponse,
|
||||||
} from "./definitions"
|
} from "./definitions"
|
||||||
import { IsolatedVM } from "../jsRunner/vm"
|
import { IsolatedVM } from "../jsRunner/vm"
|
||||||
import { iifeWrapper } from "../jsRunner/utilities"
|
import { iifeWrapper } from "@budibase/string-templates"
|
||||||
import { getIntegration } from "../integrations"
|
import { getIntegration } from "../integrations"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { context, cache, auth } from "@budibase/backend-core"
|
import { context, cache, auth } from "@budibase/backend-core"
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
"import": "./dist/bundle.mjs"
|
"import": "./dist/bundle.mjs"
|
||||||
},
|
},
|
||||||
"./package.json": "./package.json",
|
"./package.json": "./package.json",
|
||||||
"./test/utils": "./test/utils.js"
|
"./test/utils": "./test/utils.js",
|
||||||
|
"./iife": "./src/iife.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -2,6 +2,8 @@ const { atob, isBackendService, isJSAllowed } = require("../utilities")
|
||||||
const cloneDeep = require("lodash.clonedeep")
|
const cloneDeep = require("lodash.clonedeep")
|
||||||
const { LITERAL_MARKER } = require("../helpers/constants")
|
const { LITERAL_MARKER } = require("../helpers/constants")
|
||||||
const { getJsHelperList } = require("./list")
|
const { getJsHelperList } = require("./list")
|
||||||
|
const { iifeWrapper } = require("../iife")
|
||||||
|
const { CrazyLongSnippet } = require("./snippet")
|
||||||
|
|
||||||
// The method of executing JS scripts depends on the bundle being built.
|
// The method of executing JS scripts depends on the bundle being built.
|
||||||
// This setter is used in the entrypoint (either index.js or index.mjs).
|
// This setter is used in the entrypoint (either index.js or index.mjs).
|
||||||
|
@ -40,15 +42,30 @@ const getContextValue = (path, context) => {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snippets = {
|
||||||
|
Square: `
|
||||||
|
return function(num) {
|
||||||
|
return num * num
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HelloWorld: `
|
||||||
|
return "Hello, world!"
|
||||||
|
`,
|
||||||
|
CrazyLongSnippet: atob(CrazyLongSnippet),
|
||||||
|
}
|
||||||
|
|
||||||
// Evaluates JS code against a certain context
|
// Evaluates JS code against a certain context
|
||||||
module.exports.processJS = (handlebars, context) => {
|
module.exports.processJS = (handlebars, context) => {
|
||||||
|
// for testing
|
||||||
|
context.snippets = snippets
|
||||||
|
|
||||||
if (!isJSAllowed() || (isBackendService() && !runJS)) {
|
if (!isJSAllowed() || (isBackendService() && !runJS)) {
|
||||||
throw new Error("JS disabled in environment.")
|
throw new Error("JS disabled in 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.
|
||||||
const js = `(function(){${atob(handlebars)}})();`
|
const js = iifeWrapper(atob(handlebars))
|
||||||
|
|
||||||
// Our $ context function gets a value from context.
|
// Our $ context function gets a value from context.
|
||||||
// We clone the context to avoid mutation in the binding affecting real
|
// We clone the context to avoid mutation in the binding affecting real
|
||||||
|
@ -56,6 +73,16 @@ module.exports.processJS = (handlebars, context) => {
|
||||||
const sandboxContext = {
|
const sandboxContext = {
|
||||||
$: path => getContextValue(path, cloneDeep(context)),
|
$: path => getContextValue(path, cloneDeep(context)),
|
||||||
helpers: getJsHelperList(),
|
helpers: getJsHelperList(),
|
||||||
|
|
||||||
|
// Proxy to evaluate snippets when running in the browser
|
||||||
|
snippets: new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: function (_, name) {
|
||||||
|
return eval(iifeWrapper(context.snippets[name]))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a sandbox with our context and run the JS
|
// Create a sandbox with our context and run the JS
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports.iifeWrapper = script => {
|
||||||
|
return `(function(){\n${script}\n})();`
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ const handlebars = require("handlebars")
|
||||||
const { registerAll, registerMinimum } = require("./helpers/index")
|
const { registerAll, registerMinimum } = require("./helpers/index")
|
||||||
const processors = require("./processors")
|
const processors = require("./processors")
|
||||||
const { atob, btoa, isBackendService } = require("./utilities")
|
const { atob, btoa, isBackendService } = require("./utilities")
|
||||||
|
const { iifeWrapper } = require("./iife")
|
||||||
const manifest = require("../manifest.json")
|
const manifest = require("../manifest.json")
|
||||||
const {
|
const {
|
||||||
FIND_HBS_REGEX,
|
FIND_HBS_REGEX,
|
||||||
|
@ -426,3 +427,4 @@ function defaultJSSetup() {
|
||||||
defaultJSSetup()
|
defaultJSSetup()
|
||||||
|
|
||||||
module.exports.defaultJSSetup = defaultJSSetup
|
module.exports.defaultJSSetup = defaultJSSetup
|
||||||
|
module.exports.iifeWrapper = iifeWrapper
|
||||||
|
|
Loading…
Reference in New Issue