package com.oracle.truffle.js.builtins.commonjs;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.js.builtins.GlobalBuiltins;
import com.oracle.truffle.js.lang.JavaScriptLanguage;
import com.oracle.truffle.js.nodes.function.JSBuiltin;
import com.oracle.truffle.js.runtime.Errors;
import com.oracle.truffle.js.runtime.JSArguments;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.JSErrorType;
import com.oracle.truffle.js.runtime.JSException;
import com.oracle.truffle.js.runtime.JSRealm;
import com.oracle.truffle.js.runtime.JSRuntime;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.builtins.JSFunctionData;
import com.oracle.truffle.js.runtime.builtins.JSOrdinary;
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
import com.oracle.truffle.js.runtime.objects.JSObject;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.isCoreModule;
public abstract class CommonJSRequireBuiltin extends GlobalBuiltins.JSFileLoadingOperation {
private static final boolean LOG_REQUIRE_PATH_RESOLUTION = false;
private static final Stack<String> requireDebugStack;
static {
requireDebugStack = LOG_REQUIRE_PATH_RESOLUTION ? new Stack<>() : null;
}
public static void log(Object... message) {
if (LOG_REQUIRE_PATH_RESOLUTION) {
StringBuilder s = new StringBuilder("['.'");
for (String module : requireDebugStack) {
s.append(" '").append(module).append("'");
}
s.append("] ");
for (Object m : message) {
String desc;
if (m == null) {
desc = "null";
} else if (m instanceof DynamicObject) {
desc = " APIs: {" + JSObject.enumerableOwnNames((DynamicObject) m) + "}";
} else {
desc = m.toString();
}
s.append(desc);
}
System.err.println(s.toString());
}
}
private static void debugStackPush(String moduleIdentifier) {
if (LOG_REQUIRE_PATH_RESOLUTION) {
requireDebugStack.push(moduleIdentifier);
}
}
private static void debugStackPop() {
if (LOG_REQUIRE_PATH_RESOLUTION) {
requireDebugStack.pop();
}
}
public static final String FILENAME_VAR_NAME = "__filename";
public static final String DIRNAME_VAR_NAME = "__dirname";
public static final String MODULE_PROPERTY_NAME = "module";
public static final String EXPORTS_PROPERTY_NAME = "exports";
public static final String REQUIRE_PROPERTY_NAME = "require";
public static final String RESOLVE_PROPERTY_NAME = "resolve";
private static final String MODULE_END = "\n});";
private static final String MODULE_PREAMBLE = "(function (exports, require, module, __filename, __dirname) {";
private static final String LOADED_PROPERTY_NAME = "loaded";
private static final String FILENAME_PROPERTY_NAME = "filename";
private static final String ID_PROPERTY_NAME = "id";
private static final String ENV_PROPERTY_NAME = "env";
private static final String JS_EXT = ".js";
private static final String JSON_EXT = ".json";
private static final String NODE_EXT = ".node";
private final TruffleFile modulesResolutionCwd;
@TruffleBoundary
static TruffleFile getModuleResolveCurrentWorkingDirectory(JSContext context) {
TruffleLanguage.Env env = context.getRealm().getEnv();
String currentFileNameFromStack = CommonJSResolution.getCurrentFileNameFromStack();
if (currentFileNameFromStack == null) {
String cwdOption = context.getContextOptions().getRequireCwd();
return cwdOption == null ? env.getCurrentWorkingDirectory() : env.getPublicTruffleFile(cwdOption);
} else {
TruffleFile truffleFile = env.getPublicTruffleFile(currentFileNameFromStack);
assert truffleFile.isRegularFile() && truffleFile.getParent() != null;
return truffleFile.getParent().normalize();
}
}
CommonJSRequireBuiltin(JSContext context, JSBuiltin builtin) {
super(context, builtin);
this.modulesResolutionCwd = getModuleResolveCurrentWorkingDirectory(context);
}
@Specialization
protected Object require(DynamicObject currentRequire, String moduleIdentifier) {
TruffleLanguage.Env env = getContext().getRealm().getEnv();
TruffleFile resolutionEntryPath = getModuleResolutionEntryPath(currentRequire, env);
return requireImpl(moduleIdentifier, resolutionEntryPath);
}
@CompilerDirectives.TruffleBoundary
private Object requireImpl(String moduleIdentifier, TruffleFile entryPath) {
log("required module '", moduleIdentifier, "' core:", isCoreModule(moduleIdentifier), " from path ", entryPath);
if (isCoreModule(moduleIdentifier)) {
String moduleReplacementName = getContext().getContextOptions().getCommonJSRequireBuiltins().get(moduleIdentifier);
if (moduleReplacementName != null && !"".equals(moduleReplacementName)) {
return requireImpl(moduleReplacementName, modulesResolutionCwd);
}
}
TruffleFile maybeModule = CommonJSResolution.resolve(getContext(), moduleIdentifier, entryPath);
log("module ", moduleIdentifier, " resolved to ", maybeModule);
if (maybeModule == null) {
throw fail(moduleIdentifier);
}
if (isJsFile(maybeModule)) {
return evalJavaScriptFile(maybeModule, moduleIdentifier);
} else if (isJsonFile(maybeModule)) {
return evalJsonFile(maybeModule);
} else if (isNodeBinFile(maybeModule)) {
throw fail("Unsupported .node file: ", moduleIdentifier);
} else {
throw fail(moduleIdentifier);
}
}
private Object evalJavaScriptFile(TruffleFile modulePath, String moduleIdentifier) {
JSRealm realm = getContext().getRealm();
TruffleFile normalizedPath = modulePath.normalize();
Map<TruffleFile, DynamicObject> commonJSCache = realm.getCommonJSRequireCache();
if (commonJSCache.containsKey(normalizedPath)) {
DynamicObject moduleBuiltin = commonJSCache.get(normalizedPath);
Object cached = JSObject.get(moduleBuiltin, EXPORTS_PROPERTY_NAME);
log("returning cached '", modulePath, cached);
return cached;
}
Source source = sourceFromPath(modulePath.toString(), realm);
String filenameBuiltin = normalizedPath.toString();
if (modulePath.getParent() == null) {
throw fail(moduleIdentifier);
}
String dirnameBuiltin = modulePath.getParent().getAbsoluteFile().normalize().toString();
DynamicObject exportsBuiltin = createExportsBuiltin(realm);
DynamicObject moduleBuiltin = createModuleBuiltin(realm, exportsBuiltin, filenameBuiltin);
DynamicObject requireBuiltin = createRequireBuiltin(realm, moduleBuiltin, filenameBuiltin);
DynamicObject env = JSOrdinary.create(getContext());
JSObject.set(env, ENV_PROPERTY_NAME, JSOrdinary.create(getContext()));
CharSequence characters = MODULE_PREAMBLE + source.getCharacters() + MODULE_END;
Source moduleSources = Source.newBuilder(JavaScriptLanguage.ID, characters, filenameBuiltin).mimeType(JavaScriptLanguage.TEXT_MIME_TYPE).build();
CallTarget moduleCallTarget = realm.getEnv().parsePublic(moduleSources);
Object moduleExecutableFunction = moduleCallTarget.call();
if (JSFunction.isJSFunction(moduleExecutableFunction)) {
commonJSCache.put(normalizedPath, moduleBuiltin);
try {
debugStackPush(moduleIdentifier);
log("executing '", filenameBuiltin, "' for ", moduleIdentifier);
JSFunction.call(JSArguments.create(moduleExecutableFunction, moduleExecutableFunction, exportsBuiltin, requireBuiltin, moduleBuiltin, filenameBuiltin, dirnameBuiltin, env));
JSObject.set(moduleBuiltin, LOADED_PROPERTY_NAME, true);
return JSObject.get(moduleBuiltin, EXPORTS_PROPERTY_NAME);
} catch (Exception e) {
log("EXCEPTION: '", e.getMessage(), "'");
throw e;
} finally {
debugStackPop();
Object module = JSObject.get(moduleBuiltin, EXPORTS_PROPERTY_NAME);
log("done '", moduleIdentifier, "' module.exports: ", module, module);
}
}
return null;
}
private DynamicObject evalJsonFile(TruffleFile jsonFile) {
try {
if (fileExists(jsonFile)) {
Source source;
JSRealm realm = getContext().getRealm();
TruffleFile file = GlobalBuiltins.resolveRelativeFilePath(jsonFile.toString(), realm.getEnv());
if (file.isRegularFile()) {
source = sourceFromTruffleFile(file);
} else {
throw fail(jsonFile.toString());
}
DynamicObject parse = (DynamicObject) realm.getJsonParseFunctionObject();
assert source != null;
String jsonString = source.getCharacters().toString();
Object jsonObj = JSFunction.call(JSArguments.create(parse, parse, jsonString));
if (JSDynamicObject.isJSDynamicObject(jsonObj)) {
return (DynamicObject) jsonObj;
}
}
throw fail(jsonFile.toString());
} catch (SecurityException e) {
throw Errors.createErrorFromException(e);
}
}
private static JSException fail(String moduleIdentifier) {
return JSException.create(JSErrorType.TypeError, "Cannot load CommonJS module: '" + moduleIdentifier + "'");
}
@TruffleBoundary
private static JSException fail(String... message) {
StringBuilder sb = new StringBuilder();
for (String s : message) {
sb.append(s);
}
return JSException.create(JSErrorType.TypeError, sb.toString());
}
private static DynamicObject createModuleBuiltin(JSRealm realm, DynamicObject exportsBuiltin, String fileNameBuiltin) {
DynamicObject module = JSOrdinary.create(realm.getContext(), realm);
JSObject.set(module, EXPORTS_PROPERTY_NAME, exportsBuiltin);
JSObject.set(module, ID_PROPERTY_NAME, fileNameBuiltin);
JSObject.set(module, FILENAME_PROPERTY_NAME, fileNameBuiltin);
JSObject.set(module, LOADED_PROPERTY_NAME, false);
return module;
}
private static DynamicObject createRequireBuiltin(JSRealm realm, DynamicObject moduleBuiltin, String fileNameBuiltin) {
DynamicObject mainRequire = (DynamicObject) realm.getCommonJSRequireFunctionObject();
DynamicObject mainResolve = (DynamicObject) JSObject.get(mainRequire, RESOLVE_PROPERTY_NAME);
JSFunctionData functionData = JSFunction.getFunctionData(mainRequire);
DynamicObject newRequire = JSFunction.create(realm, functionData);
JSObject.set(newRequire, MODULE_PROPERTY_NAME, moduleBuiltin);
JSObject.set(newRequire, RESOLVE_PROPERTY_NAME, mainResolve);
JSObject.set(newRequire, FILENAME_VAR_NAME, fileNameBuiltin);
return newRequire;
}
private static DynamicObject createExportsBuiltin(JSRealm realm) {
return JSOrdinary.create(realm.getContext(), realm);
}
private static boolean isNodeBinFile(TruffleFile maybeModule) {
return hasExtension(Objects.requireNonNull(maybeModule.getName()), NODE_EXT);
}
private static boolean isJsFile(TruffleFile maybeModule) {
return hasExtension(Objects.requireNonNull(maybeModule.getName()), JS_EXT);
}
private static boolean isJsonFile(TruffleFile maybeModule) {
return hasExtension(Objects.requireNonNull(maybeModule.getName()), JSON_EXT);
}
private static boolean fileExists(TruffleFile modulePath) {
return modulePath.isRegularFile();
}
private TruffleFile getModuleResolutionEntryPath(DynamicObject currentRequire, TruffleLanguage.Env env) {
if (JSDynamicObject.isJSDynamicObject(currentRequire)) {
Object maybeFilename = JSObject.get(currentRequire, FILENAME_VAR_NAME);
if (JSRuntime.isString(maybeFilename)) {
String fileName = JSRuntime.toStringIsString(maybeFilename);
if (isFile(env, fileName)) {
return getParent(env, fileName);
}
}
}
return getModuleResolveCurrentWorkingDirectory(getContext());
}
private static TruffleFile getParent(TruffleLanguage.Env env, String fileName) {
return env.getPublicTruffleFile(fileName).getParent();
}
private static boolean isFile(TruffleLanguage.Env env, String fileName) {
return env.getPublicTruffleFile(fileName).exists();
}
private static boolean hasExtension(String fileName, String ext) {
return fileName.lastIndexOf(ext) > 0 && fileName.lastIndexOf(ext) == fileName.length() - ext.length();
}
}