All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.coveo.nashorn_modules.Module Maven / Gradle / Ivy

package com.coveo.nashorn_modules;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import javax.script.Bindings;
import javax.script.ScriptException;
import javax.script.SimpleBindings;

import jdk.nashorn.api.scripting.NashornScriptEngine;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import jdk.nashorn.internal.runtime.ECMAException;

public class Module extends SimpleBindings implements RequireFunction {
  private NashornScriptEngine engine;
  private Folder folder;
  private ModuleCache cache;

  private Module main;
  private Bindings module = new SimpleBindings();
  private Bindings exports = new SimpleBindings();
  private List children = new ArrayList<>();

  public Module(
      NashornScriptEngine engine,
      Folder folder,
      ModuleCache cache,
      String filename,
      Bindings global,
      Module parent,
      Module main) {

    this.engine = engine;
    this.folder = folder;
    this.cache = cache;
    this.main = main != null ? main : this;
    put("main", this.main.module);

    global.put("require", this);

    global.put("module", module);
    global.put("exports", exports);

    module.put("exports", exports);
    module.put("children", children);
    module.put("filename", filename);
    module.put("id", filename);
    module.put("loaded", false);
    module.put("parent", parent != null ? parent.module : null);
  }

  void setLoaded() {
    module.put("loaded", true);
  }

  @Override
  public Bindings require(String module) throws ScriptException {
    if (module == null) {
      throwModuleNotFoundException("");
    }

    String[] parts = Paths.splitPath(module);
    if (parts.length == 0) {
      throwModuleNotFoundException(module);
    }

    String[] folders = Arrays.copyOfRange(parts, 0, parts.length - 1);
    String[] filenames = getFilenamesToAttempt(parts[parts.length - 1]);

    Module found = null;
    if (shouldLoadFromNodeModules(module)) {
      // If the path doesn't already start with node_modules, add it.
      if (folders.length == 0 || !folders[0].equals("node_modules")) {
        folders =
            Stream.concat(Stream.of("node_modules"), Arrays.stream(folders)).toArray(String[]::new);
      }

      // When loading from node_modules, we'll try to resolve first from
      // the current folder and then we'll look at all our parents.
      Folder current = folder;
      while (current != null) {
        found = attemptToLoadStartingFromFolder(current, folders, filenames);
        if (found != null) {
          break;
        } else {
          current = current.getParent();
        }
      }
    } else {
      // When not loading from node_modules we will not automatically
      // look up the folder hierarchy, making the process quite simpler.
      found = attemptToLoadStartingFromFolder(folder, folders, filenames);
    }

    if (found == null) {
      throwModuleNotFoundException(module);
    }

    children.add(found.module);

    return found.exports;
  }

  private Module attemptToLoadStartingFromFolder(Folder from, String[] folders, String[] filenames)
      throws ScriptException {
    Folder found = resolveFolder(from, folders);
    if (found == null) {
      return null;
    }

    return attemptToLoadFromThisFolder(found, filenames);
  }

  private Module attemptToLoadFromThisFolder(Folder from, String[] filenames)
      throws ScriptException {
    for (String filename : filenames) {
      Module found = loadModule(from, filename);
      if (found != null) {
        return found;
      }
    }

    return null;
  }

  private Folder resolveFolder(Folder from, String[] folders) {
    Folder current = from;
    for (String name : folders) {
      switch (name) {
        case "":
          throw new IllegalArgumentException();
        case ".":
          continue;
        case "..":
          current = current.getParent();
          break;
        default:
          current = current.getFolder(name);
          break;
      }

      // Whenever we get stuck we bail out
      if (current == null) {
        return null;
      }
    }

    return current;
  }

  private Module loadModule(Folder parent, String name) throws ScriptException {
    String fullPath = parent.getPath() + name;

    Module found = cache.get(fullPath);

    if (found == null) {
      found = loadModuleDirectly(parent, fullPath, name);
    }

    if (found == null) {
      found = loadModuleThroughFolderName(parent, name);
    }

    if (found != null) {
      // We keep a cache entry for the requested path even though the code that
      // compiles the module also adds it to the cache with the potentially different
      // effective path. This avoids having to load package.json every time, etc.
      cache.put(fullPath, found);
    }

    return found;
  }

  private Module loadModuleDirectly(Folder parent, String fullPath, String name)
      throws ScriptException {
    String code = parent.getFile(name);
    if (code == null) {
      return null;
    }

    return compileModuleAndPutInCache(parent, fullPath, code);
  }

  private Module loadModuleThroughFolderName(Folder parent, String name) throws ScriptException {
    Folder fileAsFolder = parent.getFolder(name);
    if (fileAsFolder == null) {
      return null;
    }

    Module found = loadModuleThroughPackageJson(fileAsFolder);

    if (found == null) {
      found = loadModuleThroughIndexJs(fileAsFolder);
    }

    return found;
  }

  private Module loadModuleThroughPackageJson(Folder parent) throws ScriptException {
    String packageJson = parent.getFile("package.json");
    if (packageJson == null) {
      return null;
    }

    String mainFile = getMainFileFromPackageJson(packageJson);
    if (mainFile == null) {
      return null;
    }

    String[] parts = Paths.splitPath(mainFile);
    String[] folders = Arrays.copyOfRange(parts, 0, parts.length - 1);
    String filename = parts[parts.length - 1];
    Folder folder = resolveFolder(parent, folders);
    if (folder == null) {
      return null;
    }

    String code = folder.getFile(filename);
    return compileModuleAndPutInCache(folder, folder.getPath() + filename, code);
  }

  private Module loadModuleThroughIndexJs(Folder parent) throws ScriptException {
    String code = parent.getFile("index.js");
    if (code == null) {
      return null;
    }

    return compileModuleAndPutInCache(parent, parent.getPath() + "index.js", code);
  }

  private Module compileModuleAndPutInCache(Folder parent, String fullPath, String code)
      throws ScriptException {

    Module created;
    String lowercaseFullPath = fullPath.toLowerCase();
    if (lowercaseFullPath.endsWith(".js")) {
      created = compileJavaScriptModule(parent, fullPath, code);
    } else if (lowercaseFullPath.endsWith(".json")) {
      created = compileJsonModule(parent, fullPath, code);
    } else {
      // Unsupported module type
      return null;
    }

    // We keep a cache entry for the compiled module using it's effective path, to avoid
    // recompiling even if module is requested through a different initial path.
    cache.put(fullPath, created);

    return created;
  }

  private Module compileJavaScriptModule(Folder parent, String fullPath, String code)
      throws ScriptException {
    Bindings moduleGlobal = new SimpleBindings();
    Module created = new Module(engine, parent, cache, fullPath, moduleGlobal, this, this.main);
    engine.eval(code, moduleGlobal);
    created.setLoaded();
    return created;
  }

  private Module compileJsonModule(Folder parent, String fullPath, String code)
      throws ScriptException {
    Bindings moduleGlobal = new SimpleBindings();
    Module created = new Module(engine, parent, cache, fullPath, moduleGlobal, this, this.main);
    created.exports = parseJson(code);
    created.setLoaded();
    return created;
  }

  private String getMainFileFromPackageJson(String packageJson) throws ScriptException {
    Bindings parsed = parseJson(packageJson);
    return (String) parsed.get("main");
  }

  private Bindings parseJson(String json) throws ScriptException {
    // Pretty lame way to parse JSON but hey...
    ScriptObjectMirror jsJson = (ScriptObjectMirror) engine.eval("JSON");
    return (Bindings) jsJson.callMember("parse", json);
  }

  private void throwModuleNotFoundException(String module) throws ScriptException {
    ScriptObjectMirror ctor = (ScriptObjectMirror) engine.eval("Error");
    Bindings error = (Bindings) ctor.newObject("Module not found: " + module);
    error.put("code", "MODULE_NOT_FOUND");
    throw new ECMAException(error, null);
  }

  private static boolean shouldLoadFromNodeModules(String module) {
    return !(module.startsWith("/") || module.startsWith("../") || module.startsWith("./"));
  }

  private static String[] getFilenamesToAttempt(String filename) {
    return new String[] {filename, filename + ".js", filename + ".json"};
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy