espruino-web-ide
Version:
A Terminal and Graphical code Editor for Espruino JavaScript Microcontrollers
247 lines (221 loc) • 9.01 kB
JavaScript
/**
Copyright 2014 Gordon Williams (gw@pur3.co.uk)
This Source Code is subject to the terms of the Mozilla Public
License, v2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
------------------------------------------------------------------
Automatically load any referenced modules
------------------------------------------------------------------
**/
;
(function(){
function init() {
Espruino.Core.Config.add("MODULE_URL", {
section : "Communications",
name : "Module URL",
description : "Where to search online for modules when `require()` is used. Can supply more than one URL, separated by '|'",
type : "string",
defaultValue : "https://www.espruino.com/modules"
});
Espruino.Core.Config.add("MODULE_EXTENSIONS", {
section : "Communications",
name : "Module Extensions",
description : "The file extensions to use for each module. These are checked in order and the first that exists is used. One or more file extensions (including the dot) separated by `|`",
type : "string",
defaultValue : ".min.js|.js"
});
Espruino.Core.Config.add("MODULE_AS_FUNCTION", {
section : "Communications",
name : "Modules uploaded as functions",
description : "Espruino 1v90 and later ONLY. Upload modules as Functions, allowing any functions inside them to be loaded directly from flash when 'Save on Send' is enabled.",
type : "boolean",
defaultValue : true
});
Espruino.Core.Config.add("MODULE_PROXY_ENABLED", {
section : "Communications",
name : "Enable Proxy",
description : "Enable Proxy for loading the modules when `require()` is used (only in native IDE)",
type : "boolean",
defaultValue : false
});
Espruino.Core.Config.add("MODULE_PROXY_URL", {
section : "Communications",
name : "Proxy URL",
description : "Proxy URL for loading the modules when `require()` is used (only in native IDE)",
type : "string",
defaultValue : ""
});
Espruino.Core.Config.add("MODULE_PROXY_PORT", {
section : "Communications",
name : "Proxy Port",
description : "Proxy Port for loading the modules when `require()` is used (only in native IDE)",
type : "string",
defaultValue : ""
});
// When code is sent to Espruino, search it for modules and add extra code required to load them
Espruino.addProcessor("transformForEspruino", function(code, callback) {
loadModules(code, callback);
});
// Append the 'getModule' processor as the last (plugins get initialized after Espruino.Core modules)
Espruino.Plugins.CoreModules = {
init: function() {
Espruino.addProcessor("getModule", function(data, callback) {
if (data.moduleCode!==undefined) { // already provided be previous getModule processor
return callback(data);
}
fetchGetModule(data, callback);
});
}
};
}
function isBuiltIn(module) {
var d = Espruino.Core.Env.getData();
// If we got data from the device itself, use that as the
// definitive answer
if ("string" == typeof d.MODULES)
return d.MODULES.split(",").indexOf(module)>=0;
// Otherwise try and figure it out from JSON
if ("info" in d &&
"builtin_modules" in d.info &&
d.info.builtin_modules.indexOf(module)>=0)
return true;
// Otherwise assume we don't have it
return false;
}
/** Find any instances of require(...) in the code string and return a list */
var getModulesRequired = function(code) {
var modules = [];
var lex = Espruino.Core.Utils.getLexer(code);
var tok = lex.next();
var state = 0;
while (tok!==undefined) {
if (state==0 && tok.str=="require") {
state=1;
} else if (state==1 && tok.str=="(") {
state=2;
} else if (state==2 && (tok.type=="STRING")) {
state=0;
var module = tok.value;
if (!isBuiltIn(module) && modules.indexOf(module)<0)
modules.push(module);
} else
state = 0;
tok = lex.next();
}
return modules;
};
/** Download modules from MODULE_URL/.. */
function fetchGetModule(data, callback) {
var fullModuleName = data.moduleName;
// try and load the module the old way...
console.log("loadModule("+fullModuleName+")");
var urls = []; // Array of where to look for this module
var modName; // Simple name of the module
if(Espruino.Core.Utils.isURL(fullModuleName)) {
modName = fullModuleName.substr(fullModuleName.lastIndexOf("/") + 1).split(".")[0];
urls = [ fullModuleName ];
} else {
modName = fullModuleName;
Espruino.Config.MODULE_URL.split("|").forEach(function (url) {
url = url.trim();
if (url.length!=0)
Espruino.Config.MODULE_EXTENSIONS.split("|").forEach(function (extension) {
urls.push(url + "/" + fullModuleName + extension);
})
});
};
// Recursively go through all the urls
(function download(urls) {
if (urls.length==0) {
return callback(data);
}
var dlUrl = urls[0];
Espruino.Core.Utils.getURL(dlUrl, function (code) {
if (code!==undefined) {
// we got it!
data.moduleCode = code;
data.isMinified = dlUrl.substr(-7)==".min.js";
return callback(data);
} else {
// else try next
download(urls.slice(1));
}
});
})(urls);
}
/** Called from loadModule when a module is loaded. Parse it for other modules it might use
* and resolve dfd after all submodules have been loaded */
function moduleLoaded(resolve, requires, modName, data, loadedModuleData, alreadyMinified){
// Check for any modules used from this module that we don't already have
var newRequires = getModulesRequired(data);
console.log(" - "+modName+" requires "+JSON.stringify(newRequires));
// if we need new modules, set them to load and get their promises
var newPromises = [];
for (var i in newRequires) {
if (requires.indexOf(newRequires[i])<0) {
console.log(" Queueing "+newRequires[i]);
requires.push(newRequires[i]);
newPromises.push(loadModule(requires, newRequires[i], loadedModuleData));
} else {
console.log(" Already loading "+newRequires[i]);
}
}
var loadProcessedModule = function (module) {
// if we needed to load something, wait until it's loaded before resolving this
Promise.all(newPromises).then(function(){
// add the module to end of our array
if (Espruino.Config.MODULE_AS_FUNCTION)
loadedModuleData.push("Modules.addCached(" + JSON.stringify(module.name) + ",function(){" + module.code + "});");
else
loadedModuleData.push("Modules.addCached(" + JSON.stringify(module.name) + "," + JSON.stringify(module.code) + ");");
// We're done
resolve();
});
}
if (alreadyMinified)
loadProcessedModule({code:data,name:modName});
else
Espruino.callProcessor("transformModuleForEspruino", {code:data,name:modName}, loadProcessedModule);
}
/** Given a module name (which could be a URL), try and find it. Return
* a deferred thingybob which signals when we're done. */
function loadModule(requires, fullModuleName, loadedModuleData) {
return new Promise(function(resolve, reject) {
// First off, try and find this module using callProcessor
Espruino.callProcessor("getModule",
{ moduleName:fullModuleName, moduleCode:undefined, isMinified:false },
function(data) {
if (data.moduleCode===undefined) {
Espruino.Core.Notifications.warning("Module "+fullModuleName+" not found");
return resolve();
}
// great! it found something. Use it.
moduleLoaded(resolve, requires, fullModuleName, data.moduleCode, loadedModuleData, data.isMinified);
});
});
}
/** Finds instances of 'require' and then ensures that
those modules are loaded into the module cache beforehand
(by inserting the relevant 'addCached' commands into 'code' */
function loadModules(code, callback){
var loadedModuleData = [];
var requires = getModulesRequired(code);
if (requires.length == 0) {
// no modules needed - just return
callback(code);
} else {
Espruino.Core.Status.setStatus("Loading modules");
// Kick off the module loading (each returns a promise)
var promises = requires.map(function (moduleName) {
return loadModule(requires, moduleName, loadedModuleData);
});
// When all promises are complete
Promise.all(promises).then(function(){
callback(loadedModuleData.join("\n") + "\n" + code);
});
}
};
Espruino.Core.Modules = {
init : init
};
}());