Source: eclairjs/jvm-npm/jvm-npm.js

/**
 *  Copyright 2014 Lance Ball
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
// Since we intend to use the Function constructor.
/* jshint evil: true */

module = (typeof module == 'undefined') ? {} :  module;

(function() {
  var System  = java.lang.System,
      Scanner = java.util.Scanner,
      File    = java.io.File,
    logger = org.apache.log4j.Logger.getLogger("org.eclairjs.nashorn.resource.jvm-npm_js");

  NativeRequire = (typeof NativeRequire === 'undefined') ? {} : NativeRequire;
  if (typeof require === 'function' && !NativeRequire.require) {
    NativeRequire.require = require;
  }

  function Module(id, parent, core, modname, subdir, fullpath) {
    //print("new Module id: "+id);
    //print("new Module parent: "+parent);
    this.id = id || "";
    this.core = core || false;
    this.parent = parent;
    this.children = [];
    this.filename = id;
    this.loaded = false;
    this.body = "";
    this.modname = modname;
    this.fullpath = core && fullpath ? fullpath : id;
    this.subdir = subdir;
    this.inFolder = subdir ? true : false;
    this.zipfile = "";
    this.exportname = "";
    this.external = this.fullpath.startsWith("http");

    Object.defineProperty( this, 'exports', {
      get: function() {
        return this._exports;
      }.bind(this),
      set: function(val) {
        Require.cache[this.filename] = val;
        this._exports = val;
      }.bind(this),
    } );
    this.exports = {};

    if (parent && parent.children) parent.children.push(this);

    this.require = function(id) {
      id = id || this.id;
      return Require(id, this);
    }.bind(this);
  }

  Module.prototype.setZipfile = function(zipfile) {
    this.zipfile = zipfile || "";
  };

  Module.prototype.setExportName = function(expname) {
    this.exportname = expname || "";
  };

  Module.prototype.toString = function() {
    return JSON.stringify(this.toJSON());
  };

  Module.prototype.toJSON = function() {
    var kids = []
    for (var i = 0; i < (this.children && this.children.length ? this.children.length : 0); i++) {
        kids.push(this.children[i].modname);
    }
    return {
        id: this.id, 
        core: this.core, 
        modname: this.modname, 
        inFolder: this.inFolder, 
        subdir: this.subdir,
        zipfile: this.zipfile,
        exportname: this.exportname,
        parentid: this.parent ? this.parent.id : "",
        children: kids
    };
  };

  /*
   * We need to load JSfiles in the ScriptEngine (e.g. Nashorn) to preserve line
   * numbering in the log files for better exception reporting. If we use Module._load
   * the line numbering gets all thrown off because the script is loaded with the 
   * func.apply() instead of via the ScriptEngine.load()
   */
  Module._loadJSFile = function(file, parent, core, main, modname, subdir, fullpath, external) {
    var mod = new Module(file, parent, core, modname, subdir, fullpath);

    //var __FILENAME__ = mod.filename;
    //var dir = new File(mod.filename).getParent();

    //print("***Loading JS module: "+mod.toString());
    //print("***and fullpath: "+mod.fullpath);
    //print("***and core: "+core);

    // Save off previous module exports - if external mod doesn't have module.exports
    // explicitly defined we need to know and "module" will be whatever was loaded last
    // into Nashorn.
    var previous = module && module.exports ? module.exports : "";

    // Load the module via it's fullpath and make sure it's exports are properly
    // captured while we have them before any other require is hit.
    load(mod.fullpath);

    // A little bit of a hack - return something for exports that can serialize 
    // if not defined (for external sources that may not have module.exports defined).
    if (module && module.exports && previous === module.exports) {
        mod.exports = function(){};
    } else {
        mod.exports = module.exports;
    }

    mod.loaded = true;
    mod.main = main;

    // Cache the metadata object in ModuleUtils as only the exports
    // are cached here in require.
    ModuleUtils.addRequiredFile(mod);

    return mod.exports;
  };

  Module._load = function(file, parent, core, main, modname, subdir, fullpath) {
    var module = new Module(file, parent, core, modname, subdir, fullpath);

    var __FILENAME__ = module.filename;

    var body   = readFile(module.filename, module.core);
    var dir    = new File(module.filename).getParent();
    var args   = ['exports', 'module', 'require', '__filename', '__dirname'],
        func   = new Function(args, body);

    //print("***Calling func.apply for: " + module.toString());
    //print("***with body: "+body);
    //print("***and exports: "+module.exports);
    //print("***and filename: "+module.filename);
    //print("***and dir: "+dir);

    func.apply(module,
        [module.exports, module, module.require, module.filename, dir]);

    module.loaded = true;
    module.main = main;
    module.body = body;

    // Cache the metadata object in ModuleUtils as only the exports
    // are cached here in require.
    ModuleUtils.addRequiredFile(module);

    return module.exports;
  };

  Module.runMain = function runMain(main) {
    var file = Require.resolve(main);
    Module._load(file, undefined, false, true);
  };

  function Require(id, parent) {
    var core, native, subdir, fullpath, modname = id, file = Require.resolve(id, parent);

    // Require.resolve() will try and load the file off the local file system.  If that
    // fails it could be native require or an external source so give an attempt to load 
    // from both of those places before giving up.
    if (!file) {
      if (typeof NativeRequire.require === 'function') {
        if (Require.debug) {
          System.out.println(['Cannot resolve', id, 'defaulting to native'].join(' '));
        }
        native = NativeRequire.require(id);
        if (native) return native;
      } else {
        if (Require.debug) {
            //System.out.println(['Cannot resolve', id, 'attempting to load from external source'].join(' '));
        }
        var external = Module._loadJSFile(id, parent, core, false, modname, subdir, id);
        if (external) {
            System.out.println([id, 'was loaded from an external source'].join(' '));
            return external;
        }
      }
      System.err.println("Cannot find module " + id);
      throw new ModuleError("Cannot find module " + id, "MODULE_NOT_FOUND");
    }

    if (file.core) {
      fullpath = file.fullpath;
      file = file.path;
      core = true;
    }
    if (file.subdir) {
      subdir = file.subdir;
      file = file.path;
    }

    //print("Require file: "+file);

    try {
      if (Require.cache[file]) {
        //print("Require returing cached file: ",file);
        return Require.cache[file];
      } else if (file.endsWith('.js')) {
        return Module._loadJSFile(file, parent, core, false, modname, subdir, fullpath);
      } else if (file.endsWith('.json')) {
        return loadJSON(file);
      }
    } catch(ex) {
      if (ex instanceof java.lang.Exception) {
        throw new ModuleError("Cannot load module " + id, "LOAD_ERROR", ex);
      } else {
        print("Cannot load module " + id + " LOAD_ERROR");
        throw ex;
      }
    }
  }

  Require.resolve = function(id, parent) {
    //print("resolve id: "+id);
    //print("resolve parent: "+ (parent ? parent.id : "DNE"));

    var roots = findRoots(parent);
    for ( var i = 0 ; i < roots.length ; ++i ) {
      var root = roots[i];
      //print("root: "+root);

      var result = resolveCoreModule(id, root) ||
        resolveAsFile(id, root, '.js')   ||
        resolveAsFile(id, root, '.json') ||
        resolveAsDirectory(id, root)     ||
        resolveAsNodeModule(id, root);
      if (result) {
        //print("returning resolve result: "+result);
        if (!result.core && root !== result.slice(0, result.lastIndexOf("\/"))) {
            var subdir = result.slice(root.length+1, result.lastIndexOf("\/"));
            //print("returning resolve result subdir: "+subdir);
            return {
                path: result, 
                subdir: subdir
            };
        }
        return result;
      }
    }
    return false;
  };

  Require.root = java.lang.System.getProperty('user.dir');
  Require.NODE_PATH = undefined;

  function findRoots(parent) {
    var r = [], root = findRoot(parent);
    r.push(root);

    // Try top-level root if not found under parent root.
    r.push(Require.root);

    var paths = r.concat(Require.paths());
    //print("PATHS: "+paths);
    return paths;
  }

  function parsePaths(paths) {
    if ( ! paths ) {
      return [];
    }
    if ( paths === '' ) {
      return [];
    }
    var osName = java.lang.System.getProperty("os.name").toLowerCase();
    var separator;

    if ( osName.indexOf( 'win' ) >= 0 ) {
      separator = ';';
    } else {
      separator = ':';
    }

    return paths.split( separator );
  }

  Require.paths = function() {
    var r = [];
    r.push( java.lang.System.getProperty( "user.home" ) + "/.node_modules" );
    r.push( java.lang.System.getProperty( "user.home" ) + "/.node_libraries" );

    if ( Require.NODE_PATH ) {
      r = r.concat( parsePaths( Require.NODE_PATH ) );
    } else {
      var NODE_PATH = java.lang.System.getenv.NODE_PATH;
      if ( NODE_PATH ) {
        r = r.concat( parsePaths( NODE_PATH ) );
      }
    }
    // r.push( $PREFIX + "/node/library" );
    return r;
  };

  function findRoot(parent) {
    if (!parent || !parent.id) { return Require.root; }
    var pathParts = parent.id.split(/[\/|\\,]+/g);
    pathParts.pop();
    return pathParts.join('/');
  }

  Require.debug = true;
  //Require.debug = false;
  Require.cache = {};
  Require.extensions = {};
  require = Require;

  module.exports = Module;


  function loadJSON(file) {
    var json = JSON.parse(readFile(file));
    Require.cache[file] = json;
    return json;
  }

  function resolveAsNodeModule(id, root) {
    var base = [root, 'node_modules'].join('/');
    return resolveAsFile(id, base) ||
      resolveAsDirectory(id, base) ||
      (root ? resolveAsNodeModule(id, new File(root).getParent()) : false);
  }

  function resolveAsDirectory(id, root) {
    var base = [root, id].join('/'),
        file = new File([base, 'package.json'].join('/'));
    if (file.exists()) {
      try {
        var body = readFile(file.getCanonicalPath()),
            package  = JSON.parse(body);
        if (package.main) {
          return (resolveAsFile(package.main, base) ||
                  resolveAsDirectory(package.main, base));
        }
        // if no package.main exists, look for index.js
        return resolveAsFile('index.js', base);
      } catch(ex) {
        throw new ModuleError("Cannot load JSON file", "PARSE_ERROR", ex);
      }
    }
    return resolveAsFile('index.js', base);
  }

  function resolveAsFile(id, root, ext) {
    var file;
    if ( id.length > 0 && id[0] === '/' ) {
      file = new File(normalizeName(id, ext || '.js'));
      if (!file.exists()) {
        return resolveAsDirectory(id);
      }
    } else {
      file = new File([root, normalizeName(id, ext || '.js')].join('/'));
      //print('file: '+file);
    }
    if (file.exists()) {
      //print("########IS RESOLVED AS FILE: "+file.getName());
      return file.getCanonicalPath();
    }
  }

  function resolveCoreModule(id, root) {
    //print("resolveCoreModule id: "+id);
    //print("resolveCoreModule root: "+root);
    var name = normalizeName(id);
      var resource;
    var debugJSSourceLocation = System.getProperties().getProperty("eclairjs.jssource");
    if (debugJSSourceLocation != null) {
      resource = debugJSSourceLocation + "/" + name;
    } else {
      var classloader = java.lang.Thread.currentThread().getContextClassLoader();
      resource = classloader.getResource(name);
    }

    if (resource) {
        //print("########IS RESOLVED AS CORE: "+resource);
        return { path: name, core: true, fullpath: resource.toString() };
    }
  }

  function normalizeName(fileName, ext) {
    var extension = ext || '.js';
    if (fileName.endsWith(extension)) {
      return fileName;
    }
    return fileName + extension;
  }

  function readFile(filename, core) {
    //print('readFile: '+filename);
    var input;
    try {
      if (core) {
        var classloader = java.lang.Thread.currentThread().getContextClassLoader();
        input = classloader.getResourceAsStream(filename);
      } else {
        input = new File(filename);
      }
      // TODO: I think this is not very efficient
      return new Scanner(input).useDelimiter("\\A").next();
    } catch(e) {
      throw new ModuleError("Cannot read file ["+input+"]: ", "IO_ERROR", e);
    }
  }

  function ModuleError(message, code, cause) {
    this.code = code || "UNDEFINED";
    this.message = message || "Error loading module";
    this.cause = cause;
  }

  // Helper function until ECMAScript 6 is complete
  if (typeof String.prototype.endsWith !== 'function') {
    String.prototype.endsWith = function(suffix) {
      if (!suffix) return false;
      return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
  }

  ModuleError.prototype = new Error();
  ModuleError.prototype.constructor = ModuleError;

}());