Search code examples
javascriptembeddedduktape

How to use multiple js files with Duktape?


I'm using Duktape in embedded MCU. For test case i have: main.js file:

(function(){
    test();
})();

test.js file:

(function test(){
    print("func");
})

Both compiled as global default code and main.js is executed with duk_call(ctx, 0);

The problem is it throws error when calling a test() function.

I've also tried using just

function test() {
   print("test");
}

in test.js code, but it does not work either.

What I understand is that both files have separate execution context. That is why function is inaccessible.

But what is the right way to split code into multiple files for Duktape?

P.S. I am aiming to avoid using global context, because in documentation it is said that accessing variables is slow this way, that's why main.js looks that way.

P.P.S. I'm sure that test() function is unreachable, but I don't know how to write js code so that everything works.

P.P.P.S print() is a C function that outputs to serial port of esp32 and it works. even main.js works without a test() function call.


Solution

  • Basically, what you want is file import functionality. You can implement that in two ways:

    1. Provide a function in your backend and export that to JS, to allow loading a file dynamically at runtime.
    2. Implement module handling like in Node.js (which essentially also boils down to an import function).

    The second idea is what is used most and implements a well defined approach to include other files in your JS application. Duktape comes with an extra file that implements the require command, just like in Node.js. You only have to provide your own functions for resolving a module and to load it from disk (as duktape has not file I/O support).

    I implemented this approach in the MGA tool in MySQL Workbench. The duktape file for implementing node module handling is here. The function to resolve modules (which includes handling of nested node_modules folders etc.) is implemented in the ScriptingContext class. The relevant part of it is this:

    /**
     * Part of the module loading machinery. JS interfacing is done by the duk_module_node code.
     * But we have to do the file work here. On the stack we get the value passed to `require()` as a "module ID" and
     * the ID of the calling script (which is empty for the main script).
     */
    duk_ret_t ScriptingContext::resolveModule(duk_context *ctx) {
      // stack: [ requested_id parent_id ]
      std::string requestedID = duk_get_string(ctx, 0);
      std::string callingID = duk_get_string(ctx, 1);
      std::string parentPath = FS::isDir(callingID) ? callingID : Path::dirname(callingID);
    
      // Module resolution strategy in Node.js style: https://nodejs.org/api/modules.html#modules_all_together
      auto modules = getInternalModules();
      if (modules.find(requestedID) != modules.end()) {
        duk_push_string(ctx, requestedID.c_str());
        return 1;
      }
    
      ScriptingContext *context = ScriptingContext::fromDuktapeContext(ctx);
      std::string resolvedID;
      std::string cwd = Process::cwd();
    
      try {
        if (Path::isAbsolute(requestedID) || Utilities::hasPrefix(requestedID, ".")) {
          std::string temp;
          if (Path::isAbsolute(requestedID)) {
            temp = Path::relative(cwd, requestedID);
          } else
            temp = Path::join({ parentPath, requestedID });
    
          resolvedID = resolveFile(temp);
          if (resolvedID.empty())
            resolvedID = resolveFolder(context, temp);
        }
      } catch (std::runtime_error &e) {
        // Triggered for parse errors in package.json.
        context->throwScriptingError(ScriptingError::Syntax, e.what());
        return 0;
      }
    
      // No files found so far. Check node modules.
      if (resolvedID.empty()) {
        for (auto &folder : moduleFolders(parentPath)) {
          std::string path = Path::join({ folder, requestedID });
          std::string temp = resolveFile(path);
          if (!temp.empty()) {
            resolvedID = temp;
            break;
          }
    
          temp = resolveFolder(context, path);
          if (!temp.empty()) {
            resolvedID = temp;
            break;
          }
        }
      }
    
      if (resolvedID.empty()) {
        context->throwScriptingError(ScriptingError::Error, Utilities::format("Cannot resolve module %s", requestedID.c_str()));
        return 0;
      }
    
      duk_push_string(ctx, resolvedID.c_str());
      return 1;  // Use result on stack.
    }