9

I want my users to be able to use JavaScript as a scripting language inside my JavaScript application. In order to do so, I need to dynamically execute the source code.

There seem to be two main options for dynamically executing JavaScript:

a) Use eval(...) method ( or var func = new Function(...);) .

b) Add a <script> node to the DOM (for example by using $('body').append(...)).

Both methods work fine as long as I do not use any import statements in the dynamically executed source code. If I include import statements I get the error message Unexpected identifier.

Example user source code to be executed:

import Atom from './src/core.atom.js':

window.createTreeModel = function(){
   var root = new Atom('root');
   root.createChildAtom('child');
   return root;
}

Example application code to illustrate a possible usage of that dynamic code:

a) Using eval

var sourceCode =  editor.getText(); 
window.createTreeModel = undefined;
eval(sourceCode);
var model = window.createTreeModel();
treeView.setModel(model);

b) Using DOM modification:

var sourceCode =  editor.getText(); 
window.createTreeModel = undefined;

var script = "<script >\n"+ 
            sourceCode + "\n" +             
            "</script>";

$('body').append(script); 

var model = window.createTreeModel();
treeView.setModel(model);

If I specify no script type or use type="application/javascript" for option b), I get the Unexpected identifier error. If I use type="module" I get no error. The script tag is successfully added to the DOM, but the module code is not executed.

I first thought that might be due to asynchronous loading. However, waiting until loading of the script tag is finished did not work with type='module'. The loading mechanism works with type="application/javascript" but then ... again... import does not work.

Example code for async execution after script tag has been loaded:

function loadScript(sourceCode, callback){
        // Adding the script tag to the head as suggested before
        var head = document.getElementsByTagName('head')[0];
        var script = document.createElement('script');
        script.type = 'application/javascript';
        script.innerHTML = sourceCode;
        //script.async=false;

        // Then bind the event to the callback function.
        // There are several events for cross browser compatibility.
        script.onreadystatechange = callback;
        script.onload = callback;

        // Fire the loading
        head.appendChild(script);
    }

--

loadScript(sourceCode, function(){
        var model = window.createModel();
        console.log('model:' + model);
     });  

If I hard-code the user source code in my index.html using <source type="module">, the module code is executed. Dynamically loading the module code does not seem to work. I use Chrome version 63.0.3239.108.

=> I. How can I force the execution of the <script type="module"> tag after dynamically adding it to the DOM? or

=> II. How can I eval script that contains import (and maybe export) statements? or

=> III. What would be a good way to allow the user source code to define dependencies that can be resolved dynamically?

Related questions and articles:

Further notes:

I know that the work flow of the examples, using, window.createTreeModel is not ideal. I used it here because the code is easy to understand. I will improve my over all work flow and think about stuff like security issues ... after I managed somehow to run user source code including its dependencies.

Stefan
  • 10,010
  • 7
  • 61
  • 117
  • Use a transpiler. Don't use `window` to export, use `export` declarations. And notice that the code evaluation is probably going to be asynchronous, especially when dynamically loading dependencies, so prepare for that. – Bergi Dec 26 '17 at 12:56
  • @Bergi: Export does not work as long as the module code is not executed. Also see "Further notes". I managed to wait for the loading to be finished but that did not help. I updated my question with an example for considering the async loading. Could you please give an example on a transpiler could solve my issue? – Stefan Dec 26 '17 at 13:24
  • @Stefan He means use a transpiler to compile ES6 code to ES5 then execute the ES5 code. I'm not aware of any transpiler that works in the browser though. Most compile on your PC and you deploy ONLY ES5 code to the browser. – slebetman Dec 26 '17 at 13:42
  • @slebetman Most transpilers run anywhere (sans file loading). Have you never tried the live [Babel REPL](http://babeljs.io/repl/)? – Bergi Dec 26 '17 at 17:40
  • The linked transpiler Babel REPL translates `import` statements to `require` statements. Transforming the user source code before executing it might be a way to go. The work around in my answer below is not really beautiful. I could not avoid to use `window` since I am not able to access `exports` of the anonymous module. Nevertheless, I prefer the direct execution of the es6 code because it requires less overhead. – Stefan Dec 27 '17 at 09:01
  • 1
    Have a look at https://2ality.com/2019/10/eval-via-import.html – bennlich Apr 07 '21 at 05:09

2 Answers2

7

With data uris or objectUrls and dynamic imports:

DataURI:

const code = 'export default function hello() { console.log("Hello World"); }';
const dataUri = 'data:text/javascript;charset=utf-8,' + encodeURIComponent(code);
const module = await import(dataUri);
console.log(module); // property default contains function hello now
const myHello = module.default;
myHello(); // puts "Hello World" to console

ObjectURL:

const code = 'export default function hello() { console.log("Hello World"); }';
const objectURL = URL.createObjectURL(new Blob([code], { type: 'text/javascript' }));
const module = await import(objectURL);
console.log(module); // property default contains function hello now
const myHello = module.default;
myHello(); // puts "Hello World" to console

Imports worked in my tests to, just if you use relative paths you might have to change the directory change prefixes (like ./ or ../), but since you have to code as text first, you just can replace it with regex before emulating.

4

After adding some log messages I found out that when using type="module":

  • $('body').append(script); does not execute the module code

  • body.appendChild(script); does asynchronously execute the module code but the events onload and onreadystatechange do not work, even if I use addEventListener(...) instead of script.onload =....

Following work around works for me. It modifies the user source code to include a call to a (temporal) global callback:

    var sourceCode =  editor.getText(); 

    window.scriptLoadedHook = function(){
        var model = window.createTreeModel();
        console.log('model:' + model);
        window.scriptLoadedHook = undefined;
    };

    var body = document.body;
    var script = document.createElement('script');
    script.type = 'module';
    script.innerHTML = sourceCode + "\n" + 
                       "if(window.scriptLoadedHook){window.scriptLoadedHook();}";           
    body.appendChild(script);   

I try now to find out how to use exports from the <script type="module"> tag to at least get rid of the global function window.createModel:

How to import es6 module that has been defined in <script type="module"> tag inside html?

Stefan
  • 10,010
  • 7
  • 61
  • 117