🚀 RunJS 1.3.2

Inspired by @Apple

And very thankful for his insights and ideas I just released a custom extension for nodejs to run custom js code without having to create a custom module or a formatter.

Sometimes for small quick things you just don't want to go through the hassle of creating a full extension for a very specific thing that could be easily achieved in a few seconds of writing some JS code.

There are also other cases where a custom extension will serve the purpose for a very specific case but if there is a small variation on the logic you need to build another module i.e. map() function.

The following SC action:

Will return:

In the code field you add the JS code you want to execute and in the data grid you define the variables and their Server Connect bindings you need to pass to your script if you need them. The SC action will return the last statement executed in the code field.

Update 1.1.0

  • Remove the option to bind data to the code field
  • Add a timeout option to allow ending the execution if it takes longer than the specified limit
  • Added a checkbox to toggle if errors should be logged in the console or not
  • There was a bug with the output checkbox that defaulted to true but wasn’t working. Now it defaults to false
  • Icon changes

Update 1.2.0

Breaking Change

  • Due to wrapping the custom code in an IIFE you need to explicitly return something so it shows in the output i.e. return 'hello world'

Added

  • IIFE wrapper. Wrapped the custom code in an IIFE to improve performance of vm.
  • Forward console. You can now use console object functions i.e. console.log() in your code and the output will be forwarded to the server console.
  • UI changes

Fixed

  • Fixed initial values of several checkboxes in the UI

As always PRs are welcome to improve the extension and squash bugs :slight_smile:

27 Likes

Awesome, great idea! Thanks for sharing!

1 Like

usedModules should work now @JonL for modules auto install.

@george, thanks for the heads up.

With this module I’m still unclear if I should offer vm2 or just nodejs standard vm

The main difference between both is that vm2 allows to require modules in the constructor so people would be able to use external modules in the sandbox. For this I would indeed use the usedModules so vm2 is autoinstalled.

vm however is included in the node runtime therefore no need to install additional modules but doesn’t allow using external modules in the sandbox.

I think that if people require to use external modules in their custom script they should really go for a Custom SC module and create better user experience.

The idea for this module is just to run simple scripts to transform data to their needs and such things in a very efficient way because a lot of times I see people having to do create a lot of SC steps including several repeats that have a hit in performance.

So I think for now I am going to stick with vm and not allow people to use external modules in the sandbox and therefore no need to install a dependency as it’s included in the nodejs runtime.

For the rest of my modules I will for sure use the usedModules to improve user experience.

Well that depends on the use case indeed.

Another interesting option for heavy duty scripts is web assembly btw.

Thanks. I will take a look at wasm once you guys finalise the AC extensibility.

In the meantime @everybody I just released 1.1.0 which adds a few options.

Amazing work, Jon!

I think the way it is now, creating custom SC modules is a burden due to the lack of a boilerplate generator. Because of that, whenever possible, I'll resort to directly write JavaScript code.

I think allowing modules allows a more frictionless experience to programmers that may be comfortable writing JS code, but don't have the experience to quickly create SC modules - perhaps for those, it's just more productive to skip writing a custom module. But I understand this is a tricky topic, and each will have its own opinions. Btw, I'm talking about algorithms, not MailChimp functions

I honestly think you're a bit biased towards .map, so you only deal with a one-liner :laughing:

Have you considered following the approach where you inject the user-input JS code inside a function? So people can explictly "return" instead of relying on the last JS instruction as a return?

I've attempted to do this:


Right now, this doesn't work :frowning: it returns null

I have to do this instead:

But the "a+b+c" feels awkward without a explicit "return"

I do love my maps :smiley:

It's a limitation of vm :frowning:

https://nodejs.org/api/vm.html#scriptrunincontextcontextifiedobject-options

Returns: <any> the result of the very last statement executed in the script.

Indeed it's a tricky part. Yes it could be added easily by using vm2 instead of vm. But considering that the great majority of users are not coders and rely on Wappler on keeping their app secure I am hesitant of including the following.

  1. Run external dependencies in the sandbox that could compromise the security of it.
  2. Allow data binds in the code field to avoid injection of malicious code coming from the database or another source.
1 Like

If you need to return the result of a function assign it to a variable:

ret = function() {
 let c = 1
 return a+b+c
}()
1 Like
const vm = require('vm')
exports.run = async function (options) {
    const code = "ret = function() { " + options.code + " }()";
    ...
}

:upside_down_face:

Maybe one more option to select user preference? "Run within function"

WOW! this is awesome… thanks for doing this JonL

1 Like

Sticking to the standard means less maintenance and problems if node guys decide to introduce changes to vm.

Additionally I need to understand better how promise handling works in VM context so I have to test that before prepending a variable to a function.

Is it unbearable for your eyes having to add a variable and assign it to the function result? I understand it’s kind of unsettling not being able to use return in the main code. I believe this is by design to secure the sandbox.

I am more concerned about the lack of a proper control in the UI that handles code although this is expected as nobody has asked for it…until now :slight_smile:

@george do you guys have by any chance an undocumented control for code with linting and beautifier?

1 Like

Not a problem!

There's a bug with the Output checkbox: it comes ticked by default, but it doesn't really output anything. If I untick and tick it again it works

Edit: As a new feature, is it possible to define the schema of the output? Like what happens with API Action or Set Value (this last one's broken due to a bug)

Regarding the output toggling this should be “fixed” with the last version 1.1.0
I changed it to default to false.

Related to:

For custom extensions the output schema is defined in a json file and it’s static but it’s true that it is available for the API action. @george is this undocumented or not available for custom modules?

Update 1.2.0

Breaking Change

  • Due to wrapping the custom code in an IIFE you need to explicitly return something so it shows in the output i.e. return 'hello world'

Added

  • IIFE wrapper. Wrapped the custom code in an IIFE to improve performance of vm.
  • Forward console. You can now use console object functions in your code i.e. console.log() and the output will be forwarded to the server console.
  • UI changes

Fixed

  • Fixed initial values of several checkboxes in the UI
1 Like

@JonL I’m struggling with getting it to work, I’m not sure if I am using the extension wrongly or if my code is wrong.
I do think this is the exact use case for your extension though, need to apply a little bit of JS to manipulate data…

  1. This is my data, from a db query:
 "query_products": [
    {
      "id": 13,
      "ac_tags": "tag1, tag2"

    },
    {
      "id": 14,
      "ac_tags": "tag3"
    }
  ],
  1. I want to loop over the data and for each product: split the ac_tags and then push the values into one array. Expected outcome:
    acTagsArr = ["tag1, tag2, tag3"];

  2. I’m trying to get this done with the following JS:

acTags = []; //declare my output var
tagsSplit = tags.split(','); //Split the 'ac_tags' so I get an array where 1 tag = 1 item
acTags.push(...tagsSplit); //push the split items into the output var

I’m expecting an output of: acTagsArr = ["tag1", "tag2", "tag3"] (still need to remove the comma and make it a single string.)

  1. These are the settings of the plugin

  2. This is the location (in a repeat)

  3. This is my current output:

"repeat_push_to_array": [
    {
    },
    {
    }
  ]

What version of RunJS are you running?

This code should work with the latest 1.2.0:

return tags.split(",")

Also note you have a space after each comma (double-check if this is the case), so this is the code:

return tags.split(", ")

For previous versions of RunJS, omit the return keyword:

tags.split(",");

The method .split() already returns an Array, so you can return it directly without creating acTags2 :slight_smile:

Edit: Actually, your problem is you’re not actually returning acTags

acTags = []; //declare my output var
tagsSplit = tags.split(','); //Split the 'ac_tags' so I get an array where 1 tag = 1 item
acTags.push(...tagsSplit); //push the split items into the output var

return acTags; // RunJS 1.2.0
acTags; // Previous RunJS versions
1 Like

Thanks @Apple appreciate the help!

I got a bit further, I didn’t realise we need to use return now.

With this code:

return tags.split(",").map(item => item.trim());

I get:

 "repeatACtags": [
    {
      "ac_tags": [
        "tag1"
      ]
    },
    {
      "ac_tags": [
        "tag2",
        "tag3"
      ]
    }
  ]

so now I want to make it one array, which I’ve done before in a similar way:

This is outputting nothing. I’m guessing because I’m not passing the acTagsArr to the RunJS. But if I do that I think my variable will stay within this scope of this RunJS.

Any idea?

Your array declaration (#1) doesn’t matter to RunJS (even if you pass it in Data), the scope is isolated as you guessed (pass by value, not by reference)

Similarly, the step #3 to #1 doesn’t happen - the value returned by RunJS is not magically filled into acTagsArr (RunJS has no reference to it). You need to put a Set Value acTagsArr after the RunJS step, and you select in the Data Binding Picker the value RunJS returns