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 patibility.
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:
Execute JavaScript code stored as a string
/
How do I include a JavaScript file in another JavaScript file?
Why is script.onload not working in a Chrome userscript?
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.
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 patibility.
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:
Execute JavaScript code stored as a string
https://www.html5rocks./en/tutorials/security/sandboxed-iframes/#safely-sandboxing-eval
https://javascriptweblog.wordpress./2010/04/19/how-evil-is-eval/
How do I include a JavaScript file in another JavaScript file?
Why is script.onload not working in a Chrome userscript?
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.
-
Use a transpiler. Don't use
window
to export, useexport
declarations. And notice that the code evaluation is probably going to be asynchronous, especially when dynamically loading dependencies, so prepare for that. – Bergi Commented Dec 26, 2017 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 Commented Dec 26, 2017 at 13:24
- @Stefan He means use a transpiler to pile ES6 code to ES5 then execute the ES5 code. I'm not aware of any transpiler that works in the browser though. Most pile on your PC and you deploy ONLY ES5 code to the browser. – slebetman Commented Dec 26, 2017 at 13:42
- @slebetman Most transpilers run anywhere (sans file loading). Have you never tried the live Babel REPL? – Bergi Commented Dec 26, 2017 at 17:40
- 2 Have a look at 2ality./2019/10/eval-via-import.html – bennlich Commented Apr 7, 2021 at 5:09
2 Answers
Reset to default 10With 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.
After adding some log messages I found out that when using type="module"
:
$('body').append(script);
does not execute the module codebody.appendChild(script);
does asynchronously execute the module code but the eventsonload
andonreadystatechange
do not work, even if I use addEventListener(...) instead ofscript.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?