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

org.htmlunit.corejs.javascript.commonjs.module.Require Maven / Gradle / Ivy

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.htmlunit.corejs.javascript.commonjs.module;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.htmlunit.corejs.javascript.BaseFunction;
import org.htmlunit.corejs.javascript.Context;
import org.htmlunit.corejs.javascript.Script;
import org.htmlunit.corejs.javascript.ScriptRuntime;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.ScriptableObject;

/**
 * Implements the require() function as defined by Common JS modules.
 *
 * 

Thread safety

* * You will ordinarily create one instance of require() for every top-level scope. This ordinarily * means one instance per program execution, except if you use shared top-level scopes and * installing most objects into them. Module loading is thread safe, so using a single require() in * a shared top-level scope is also safe. * *

Creation

* * If you need to create many otherwise identical require() functions for different scopes, you * might want to use {@link RequireBuilder} for convenience. * *

Making it available

* * In order to make the require() function available to your JavaScript program, you need to invoke * either {@link #install(Scriptable)} or {@link #requireMain(Context, String)}. * * @author Attila Szegedi * @version $Id: Require.java,v 1.4 2011/04/07 20:26:11 hannes%helma.at Exp $ */ public class Require extends BaseFunction { private static final long serialVersionUID = 1L; private final ModuleScriptProvider moduleScriptProvider; private final Scriptable nativeScope; private final Scriptable paths; private final boolean sandboxed; private final Script preExec; private final Script postExec; private String mainModuleId = null; private Scriptable mainExports; // Modules that completed loading; visible to all threads private final Map exportedModuleInterfaces = new ConcurrentHashMap<>(); private final Object loadLock = new Object(); // Modules currently being loaded on the thread. Used to resolve circular // dependencies while loading. private static final ThreadLocal> loadingModuleInterfaces = new ThreadLocal<>(); /** * Creates a new instance of the require() function. Upon constructing it, you will either want * to install it in the global (or some other) scope using {@link #install(Scriptable)}, or * alternatively, you can load the program's main module using {@link #requireMain(Context, * String)} and then act on the main module's exports. * * @param cx the current context * @param nativeScope a scope that provides the standard native JavaScript objects. * @param moduleScriptProvider a provider for module scripts * @param preExec an optional script that is executed in every module's scope before its module * script is run. * @param postExec an optional script that is executed in every module's scope after its module * script is run. * @param sandboxed if set to true, the require function will be sandboxed. This means that it * doesn't have the "paths" property, and also that the modules it loads don't export the * "module.uri" property. */ public Require( Context cx, Scriptable nativeScope, ModuleScriptProvider moduleScriptProvider, Script preExec, Script postExec, boolean sandboxed) { this.moduleScriptProvider = moduleScriptProvider; this.nativeScope = nativeScope; this.sandboxed = sandboxed; this.preExec = preExec; this.postExec = postExec; setPrototype(ScriptableObject.getFunctionPrototype(nativeScope)); if (!sandboxed) { paths = cx.newArray(nativeScope, 0); defineReadOnlyProperty(this, "paths", paths); } else { paths = null; } } /** * Calling this method establishes a module as being the main module of the program to which * this require() instance belongs. The module will be loaded as if require()'d and its "module" * property will be set as the "main" property of this require() instance. You have to call this * method before the module has been loaded (that is, the call to this method must be the first * to require the module and thus trigger its loading). Note that the main module will execute * in its own scope and not in the global scope. Since all other modules see the global scope, * executing the main module in the global scope would open it for tampering by other modules. * * @param cx the current context * @param mainModuleId the ID of the main module * @return the "exports" property of the main module * @throws IllegalStateException if the main module is already loaded when required, or if this * require() instance already has a different main module set. */ public Scriptable requireMain(Context cx, String mainModuleId) { if (this.mainModuleId != null) { if (!this.mainModuleId.equals(mainModuleId)) { throw new IllegalStateException("Main module already set to " + this.mainModuleId); } return mainExports; } ModuleScript moduleScript; try { // try to get the module script to see if it is on the module path moduleScript = moduleScriptProvider.getModuleScript(cx, mainModuleId, null, null, paths); } catch (RuntimeException x) { throw x; } catch (Exception x) { throw new RuntimeException(x); } if (moduleScript != null) { mainExports = getExportedModuleInterface(cx, mainModuleId, null, null, true); } else if (!sandboxed) { URI mainUri = null; // try to resolve to an absolute URI or file path try { mainUri = new URI(mainModuleId); } catch (URISyntaxException usx) { // fall through } // if not an absolute uri resolve to a file path if (mainUri == null || !mainUri.isAbsolute()) { File file = new File(mainModuleId); if (!file.isFile()) { throw ScriptRuntime.throwError( cx, nativeScope, "Module \"" + mainModuleId + "\" not found."); } mainUri = file.toURI(); } mainExports = getExportedModuleInterface(cx, mainUri.toString(), mainUri, null, true); } this.mainModuleId = mainModuleId; return mainExports; } /** * Binds this instance of require() into the specified scope under the property name "require". * * @param scope the scope where the require() function is to be installed. */ public void install(Scriptable scope) { ScriptableObject.putProperty(scope, "require", this); } @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (args == null || args.length < 1) { throw ScriptRuntime.throwError(cx, scope, "require() needs one argument"); } String id = (String) Context.jsToJava(args[0], String.class); URI uri = null; URI base = null; if (id.startsWith("./") || id.startsWith("../")) { if (!(thisObj instanceof ModuleScope)) { throw ScriptRuntime.throwError( cx, scope, "Can't resolve relative module ID \"" + id + "\" when require() is used outside of a module"); } ModuleScope moduleScope = (ModuleScope) thisObj; base = moduleScope.getBase(); URI current = moduleScope.getUri(); uri = current.resolve(id); if (base == null) { // calling module is absolute, resolve to absolute URI // (but without file extension) id = uri.toString(); } else { // try to convert to a relative URI rooted on base id = base.relativize(current).resolve(id).toString(); if (id.charAt(0) == '.') { // resulting URI is not contained in base, // throw error or make absolute depending on sandbox flag. if (sandboxed) { throw ScriptRuntime.throwError( cx, scope, "Module \"" + id + "\" is not contained in sandbox."); } id = uri.toString(); } } } return getExportedModuleInterface(cx, id, uri, base, false); } @Override public Scriptable construct(Context cx, Scriptable scope, Object[] args) { throw ScriptRuntime.throwError(cx, scope, "require() can not be invoked as a constructor"); } private Scriptable getExportedModuleInterface( Context cx, String id, URI uri, URI base, boolean isMain) { // Check if the requested module is already completely loaded Scriptable exports = exportedModuleInterfaces.get(id); if (exports != null) { if (isMain) { throw new IllegalStateException("Attempt to set main module after it was loaded"); } return exports; } // Check if it is currently being loaded on the current thread // (supporting circular dependencies). Map threadLoadingModules = loadingModuleInterfaces.get(); if (threadLoadingModules != null) { exports = threadLoadingModules.get(id); if (exports != null) { return exports; } } // The requested module is neither already loaded, nor is it being // loaded on the current thread. End of fast path. We must synchronize // now, as we have to guarantee that at most one thread can load // modules at any one time. Otherwise, two threads could end up // attempting to load two circularly dependent modules in opposite // order, which would lead to either unacceptable non-determinism or // deadlock, depending on whether we underprotected or overprotected it // with locks. synchronized (loadLock) { // Recheck if it is already loaded - other thread might've // completed loading it just as we entered the synchronized block. exports = exportedModuleInterfaces.get(id); if (exports != null) { return exports; } // Nope, still not loaded; we're loading it then. final ModuleScript moduleScript = getModule(cx, id, uri, base); if (sandboxed && !moduleScript.isSandboxed()) { throw ScriptRuntime.throwError( cx, nativeScope, "Module \"" + id + "\" is not contained in sandbox."); } exports = cx.newObject(nativeScope); // Are we the outermost locked invocation on this thread? final boolean outermostLocked = threadLoadingModules == null; if (outermostLocked) { threadLoadingModules = new HashMap<>(); loadingModuleInterfaces.set(threadLoadingModules); } // Must make the module exports available immediately on the // current thread, to satisfy the CommonJS Modules/1.1 requirement // that "If there is a dependency cycle, the foreign module may not // have finished executing at the time it is required by one of its // transitive dependencies; in this case, the object returned by // "require" must contain at least the exports that the foreign // module has prepared before the call to require that led to the // current module's execution." threadLoadingModules.put(id, exports); try { // Support non-standard Node.js feature to allow modules to // replace the exports object by setting module.exports. Scriptable newExports = executeModuleScript(cx, id, exports, moduleScript, isMain); if (exports != newExports) { threadLoadingModules.put(id, newExports); exports = newExports; } } catch (RuntimeException e) { // Throw loaded module away if there was an exception threadLoadingModules.remove(id); throw e; } finally { if (outermostLocked) { // Make loaded modules visible to other threads only after // the topmost triggering load has completed. This strategy // (compared to the one where we'd make each module // globally available as soon as it loads) prevents other // threads from observing a partially loaded circular // dependency of a module that completed loading. exportedModuleInterfaces.putAll(threadLoadingModules); loadingModuleInterfaces.set(null); } } } return exports; } private Scriptable executeModuleScript( Context cx, String id, Scriptable exports, ModuleScript moduleScript, boolean isMain) { final ScriptableObject moduleObject = (ScriptableObject) cx.newObject(nativeScope); URI uri = moduleScript.getUri(); URI base = moduleScript.getBase(); defineReadOnlyProperty(moduleObject, "id", id); if (!sandboxed) { defineReadOnlyProperty(moduleObject, "uri", uri.toString()); } final Scriptable executionScope = new ModuleScope(nativeScope, uri, base); // Set this so it can access the global JS environment objects. // This means we're currently using the "MGN" approach (ModuleScript // with Global Natives) as specified here: // executionScope.put("exports", executionScope, exports); executionScope.put("module", executionScope, moduleObject); moduleObject.put("exports", moduleObject, exports); install(executionScope); if (isMain) { defineReadOnlyProperty(this, "main", moduleObject); } executeOptionalScript(preExec, cx, executionScope); moduleScript.getScript().exec(cx, executionScope); executeOptionalScript(postExec, cx, executionScope); return ScriptRuntime.toObject( cx, nativeScope, ScriptableObject.getProperty(moduleObject, "exports")); } private static void executeOptionalScript( Script script, Context cx, Scriptable executionScope) { if (script != null) { script.exec(cx, executionScope); } } private static void defineReadOnlyProperty(ScriptableObject obj, String name, Object value) { ScriptableObject.putProperty(obj, name, value); obj.setAttributes(name, ScriptableObject.READONLY | ScriptableObject.PERMANENT); } private ModuleScript getModule(Context cx, String id, URI uri, URI base) { try { final ModuleScript moduleScript = moduleScriptProvider.getModuleScript(cx, id, uri, base, paths); if (moduleScript == null) { throw ScriptRuntime.throwError(cx, nativeScope, "Module \"" + id + "\" not found."); } return moduleScript; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw Context.throwAsScriptRuntimeEx(e); } } @Override public String getFunctionName() { return "require"; } @Override public int getArity() { return 1; } @Override public int getLength() { return 1; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy