How to dynamically execute/eval JavaScript code that contains an ES6 module / requires some dependencies?

5k views Asked by At

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.

2

There are 2 answers

1
AudioBubble On BEST ANSWER

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.

0
Stefan On

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?