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:
https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/#safely-sandboxing-eval
https://javascriptweblog.wordpress.com/2010/04/19/how-evil-is-eval/
How do I include a JavaScript file in another JavaScript file?
How to import es6 module that has been defined in <script type=”module”> tag inside html?
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.
Advertisement
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.