package com.oracle.truffle.js.builtins.commonjs;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.js.runtime.Errors;
import com.oracle.truffle.js.runtime.JSArguments;
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.objects.DefaultESModuleLoader;
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
import com.oracle.truffle.js.runtime.objects.JSModuleRecord;
import com.oracle.truffle.js.runtime.objects.JSObject;
import com.oracle.truffle.js.runtime.objects.ScriptOrModule;
import com.oracle.truffle.js.runtime.objects.Undefined;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystemNotFoundException;
import java.util.List;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSRequireBuiltin.log;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.PACKAGE_JSON;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.PACKAGE_JSON_MAIN_PROPERTY_NAME;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.PACKAGE_JSON_MODULE_VALUE;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.PACKAGE_JSON_TYPE_PROPERTY_NAME;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.getNodeModulesPaths;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.isCoreModule;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.joinPaths;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.loadAsFile;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.loadIndex;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.loadJsonObject;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID;
import static com.oracle.truffle.js.lang.JavaScriptLanguage.MODULE_SOURCE_NAME_SUFFIX;
public final class NpmCompatibleESModuleLoader extends DefaultESModuleLoader {
public static NpmCompatibleESModuleLoader create(JSRealm realm) {
return new NpmCompatibleESModuleLoader(realm);
}
private NpmCompatibleESModuleLoader(JSRealm realm) {
super(realm);
}
@TruffleBoundary
@Override
public JSModuleRecord resolveImportedModule(ScriptOrModule referencingModule, String specifier) {
log("IMPORT resolve ", specifier);
if (isCoreModule(specifier)) {
return loadCoreModule(specifier);
}
try {
TruffleFile file = resolveURL(referencingModule, specifier);
return loadModuleFromUrl(specifier, file, file.getPath());
} catch (IOException e) {
log("IMPORT resolve ", specifier, " FAILED ", e.getMessage());
throw Errors.createErrorFromException(e);
}
}
private JSModuleRecord loadCoreModule(String specifier) {
log("IMPORT resolve built-in ", specifier);
JSModuleRecord existingModule = moduleMap.get(specifier);
if (existingModule != null) {
log("IMPORT resolve built-in from cache ", specifier);
return existingModule;
}
String moduleReplacementName = realm.getContext().getContextOptions().getCommonJSRequireBuiltins().get(specifier);
Source src;
if (moduleReplacementName != null && moduleReplacementName.endsWith(MODULE_SOURCE_NAME_SUFFIX)) {
try {
String cwdOption = realm.getContext().getContextOptions().getRequireCwd();
TruffleFile cwd = cwdOption == null ? realm.getEnv().getCurrentWorkingDirectory() : realm.getEnv().getPublicTruffleFile(cwdOption);
TruffleFile modulePath = joinPaths(realm.getEnv(), cwd, moduleReplacementName);
src = Source.newBuilder(ID, modulePath).build();
} catch (IOException | SecurityException e) {
throw fail("Failed to load built-in ES module: " + specifier + ". " + e.getMessage());
}
} else {
DynamicObject require = (DynamicObject) realm.getCommonJSRequireFunctionObject();
Object maybeModule = JSFunction.call(JSArguments.create(Undefined.instance, require, specifier));
if (maybeModule == Undefined.instance || !JSDynamicObject.isJSDynamicObject(maybeModule)) {
throw fail("Failed to load built-in ES module: " + specifier);
}
DynamicObject module = (DynamicObject) maybeModule;
List<String> exportedValues = JSObject.enumerableOwnNames(module);
StringBuilder moduleBody = new StringBuilder();
moduleBody.append("const builtinModule = require('" + specifier + "');\n");
for (String s : exportedValues) {
moduleBody.append("export const " + s + " = builtinModule." + s + ";\n");
}
moduleBody.append("export default builtinModule;");
src = Source.newBuilder(ID, moduleBody.toString(), specifier + "-internal.mjs").build();
}
JSModuleRecord record = realm.getContext().getEvaluator().parseModule(realm.getContext(), src, this);
moduleMap.put(specifier, record);
return record;
}
private TruffleFile resolveURL(ScriptOrModule referencingModule, String specifier) {
if (specifier.isEmpty()) {
throw fail(specifier);
}
TruffleLanguage.Env env = realm.getEnv();
TruffleFile resolvedUrl = null;
URI maybeUri = asURI(specifier);
if (maybeUri != null) {
try {
resolvedUrl = env.getPublicTruffleFile(maybeUri);
} catch (FileSystemNotFoundException e) {
throw failMessage("Only file:// urls are supported: " + e.getMessage());
}
} else if (specifier.charAt(0) == '/') {
resolvedUrl = env.getPublicTruffleFile(specifier);
} else if (isRelativePathFileName(specifier)) {
TruffleFile fullPath = getParentPath(referencingModule);
if (fullPath == null) {
throw fail(specifier);
}
resolvedUrl = joinPaths(env, fullPath, specifier);
} else {
resolvedUrl = packageResolve(specifier, referencingModule);
}
assert resolvedUrl != null;
if (resolvedUrl.toString().toUpperCase().contains("%2F") || resolvedUrl.toString().toUpperCase().contains("%5C")) {
throw fail(specifier);
}
if (!resolvedUrl.endsWith("/") && !resolvedUrl.exists()) {
throw fail(specifier);
}
return resolvedUrl;
}
private TruffleFile getParentPath(ScriptOrModule referencingModule) {
String refPath = referencingModule == null ? null : referencingModule.getSource().getPath();
if (refPath == null) {
return realm.getEnv().getPublicTruffleFile(realm.getContext().getContextOptions().getRequireCwd());
}
return realm.getEnv().getPublicTruffleFile(refPath).getParent();
}
private TruffleFile getFullPath(ScriptOrModule referencingModule) {
String refPath = referencingModule == null ? null : referencingModule.getSource().getPath();
if (refPath == null) {
refPath = realm.getContext().getContextOptions().getRequireCwd();
}
return realm.getEnv().getPublicTruffleFile(refPath);
}
private TruffleFile packageResolve(String packageSpecifier, ScriptOrModule referencingModule) {
TruffleLanguage.Env env = realm.getEnv();
String packageName = null;
if (packageSpecifier.isEmpty()) {
throw fail(packageSpecifier);
}
if (packageSpecifier.indexOf('/') == -1) {
packageName = packageSpecifier;
} else {
throw fail(packageSpecifier);
}
if (packageName.charAt(0) == '.') {
throw fail(packageSpecifier);
}
TruffleFile mainPackageFolder = getFullPath(referencingModule);
List<TruffleFile> nodeModulesPaths = getNodeModulesPaths(mainPackageFolder);
for (TruffleFile modulePath : nodeModulesPaths) {
TruffleFile moduleFolder = joinPaths(env, modulePath, packageSpecifier);
TruffleFile packageJson = joinPaths(env, moduleFolder, PACKAGE_JSON);
if (CommonJSResolution.fileExists(packageJson)) {
DynamicObject jsonObj = loadJsonObject(packageJson, realm.getContext());
if (JSDynamicObject.isJSDynamicObject(jsonObj)) {
Object main = JSObject.get(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME);
Object type = JSObject.get(jsonObj, PACKAGE_JSON_TYPE_PROPERTY_NAME);
if (type == Undefined.instance || !JSRuntime.isString(type) || !PACKAGE_JSON_MODULE_VALUE.equals(JSRuntime.safeToString(type))) {
throw failMessage("do not use import() to load non-ES modules.");
}
if (!JSRuntime.isString(main)) {
return loadIndex(env, moduleFolder);
}
TruffleFile mainPackageFile = joinPaths(env, moduleFolder, JSRuntime.safeToString(main));
TruffleFile asFile = loadAsFile(env, mainPackageFile);
if (asFile != null) {
return asFile;
} else {
return loadIndex(env, mainPackageFile);
}
}
}
}
TruffleFile maybeFile = env.getPublicTruffleFile(packageSpecifier);
if (maybeFile.exists()) {
return maybeFile;
}
throw fail(packageSpecifier);
}
@TruffleBoundary
private static JSException failMessage(String message) {
return JSException.create(JSErrorType.TypeError, message);
}
@TruffleBoundary
private static JSException fail(String moduleIdentifier) {
return failMessage("Cannot load module: '" + moduleIdentifier + "'");
}
private static boolean isRelativePathFileName(String moduleIdentifier) {
return moduleIdentifier.startsWith("./") || moduleIdentifier.startsWith("../");
}
}