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

com.oracle.truffle.js.builtins.commonjs.CommonJSRequireBuiltin Maven / Gradle / Ivy

/*
 * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.js.builtins.commonjs;

import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.CJS_EXT;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.JSON_EXT;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.JS_EXT;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.MJS_EXT;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.NODE_EXT;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.getCoreModuleReplacement;

import java.util.Map;
import java.util.Objects;
import java.util.Stack;

import com.oracle.truffle.api.CallTarget;
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.Fallback;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.js.builtins.GlobalBuiltins;
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.Strings;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.builtins.JSFunctionData;
import com.oracle.truffle.js.runtime.builtins.JSFunctionObject;
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 com.oracle.truffle.js.runtime.objects.Undefined;

public abstract class CommonJSRequireBuiltin extends GlobalBuiltins.JSFileLoadingOperation {

    private static final boolean LOG_REQUIRE_PATH_RESOLUTION = false;
    private static final Stack requireDebugStack;

    private static final String MODULE_PREAMBLE_PREFIX = "(function (";
    private static final String MODULE_PREAMBLE_POST = ") {";
    private static final String MODULE_END = "});";
    private static final String MODULE_FUNCTION_ARGS = "exports, require, module, __filename, __dirname";

    public static final String UNSUPPORTED_NODE_FILE = "Unsupported .node file: ";

    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 JSDynamicObject) {
                    desc = "   APIs: {" + JSObject.enumerableOwnNames((JSDynamicObject) 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();
        }
    }

    @TruffleBoundary
    static TruffleFile getModuleResolveCurrentWorkingDirectory(JSRealm realm, TruffleLanguage.Env env) {
        String currentFileNameFromStack = CommonJSResolution.getCurrentFileNameFromStack();
        if (currentFileNameFromStack != null) {
            TruffleFile truffleFile = env.getPublicTruffleFile(currentFileNameFromStack);
            if (truffleFile.isRegularFile() && truffleFile.getParent() != null) {
                return truffleFile.getParent().normalize();
            }
        }
        return getRequireCwd(realm, env);
    }

    static TruffleFile getRequireCwd(JSRealm realm, TruffleLanguage.Env env) {
        String cwdOption = realm.getContextOptions().getRequireCwd();
        return cwdOption.isEmpty() ? env.getCurrentWorkingDirectory() : env.getPublicTruffleFile(cwdOption);
    }

    CommonJSRequireBuiltin(JSContext context, JSBuiltin builtin) {
        super(context, builtin);
    }

    @TruffleBoundary
    @Specialization
    protected Object require(JSDynamicObject currentRequire, TruffleString moduleIdentifier) {
        JSRealm realm = getRealm();
        TruffleLanguage.Env env = realm.getEnv();
        String moduleIdentifierJavaString = moduleIdentifier.toJavaStringUncached();
        try {
            TruffleFile resolutionEntryPath = getModuleResolutionEntryPath(currentRequire, realm, env);
            return requireImpl(moduleIdentifierJavaString, resolutionEntryPath, realm);
        } catch (SecurityException | UnsupportedOperationException | IllegalArgumentException e) {
            throw fail(moduleIdentifierJavaString, e.getMessage());
        }
    }

    @Fallback
    protected static Object fallback(@SuppressWarnings("unused") Object function, Object moduleIdentifier) {
        throw Errors.createTypeErrorNotAString(moduleIdentifier);
    }

    @TruffleBoundary
    private Object requireImpl(String moduleIdentifier, TruffleFile entryPath, JSRealm realm) {
        log("required module '", moduleIdentifier, "' from path ", entryPath);
        String moduleReplacementName = getCoreModuleReplacement(realm, moduleIdentifier);
        if (moduleReplacementName != null) {
            log("using module replacement for module '", moduleIdentifier, "' with ", moduleReplacementName);
            return requireImpl(moduleReplacementName, getRequireCwd(realm, realm.getEnv()), realm);
        } // no core module replacement alias was found: continue and search in the FS.
        TruffleFile maybeModule;
        try {
            maybeModule = CommonJSResolution.resolve(realm, moduleIdentifier, entryPath);
        } catch (SecurityException | IllegalArgumentException | UnsupportedOperationException e) {
            // Module resolution does not execute JS code. Therefore, an exception at this stage is
            // either IO-related (e.g., file not found) or was raised in a custom Truffle FS.
            // We treat any exception as a module loading failure.
            throw fail(moduleIdentifier, e.getMessage());
        }
        log("module ", moduleIdentifier, " resolved to ", maybeModule);

        if (maybeModule == null) {
            // A custom Truffle FS might still try to map a package specifier to some file.
            TruffleFile maybeCustom = realm.getEnv().getPublicTruffleFile(moduleIdentifier);
            if (maybeCustom.exists()) {
                maybeModule = maybeCustom;
            } else {
                throw fail(moduleIdentifier);
            }
        }
        if (isJsFile(maybeModule) || isCjsFile(maybeModule)) {
            return evalJavaScriptFile(maybeModule, moduleIdentifier);
        } else if (isJsonFile(maybeModule)) {
            return evalJsonFile(maybeModule);
        } else if (isNodeBinFile(maybeModule)) {
            throw fail(UNSUPPORTED_NODE_FILE, moduleIdentifier);
        } else if (maybeModule.exists() && !isMjsFile(maybeModule)) {
            // No extension matched: still try loading as a CJS file
            return evalJavaScriptFile(maybeModule, moduleIdentifier);
        } else {
            throw fail(moduleIdentifier);
        }
    }

    private Object evalJavaScriptFile(TruffleFile modulePath, String moduleIdentifier) {
        JSRealm realm = getRealm();
        TruffleFile normalizedPath = modulePath.normalize();
        // If cached, return from cache. This is by design to avoid infinite require loops.
        Map commonJSCache = realm.getCommonJSRequireCache();
        if (commonJSCache.containsKey(normalizedPath)) {
            JSDynamicObject moduleBuiltin = commonJSCache.get(normalizedPath);
            Object cached = JSObject.get(moduleBuiltin, Strings.EXPORTS_PROPERTY_NAME);
            log("returning cached '", modulePath, cached);
            return cached;
        }
        // Read the file.
        Source source = sourceFromPath(modulePath.toString(), realm);
        TruffleString filenameBuiltin = Strings.fromJavaString(normalizedPath.toString());
        if (modulePath.getParent() == null && !modulePath.exists()) {
            throw fail(moduleIdentifier);
        }
        // Create `require` and other builtins for this module.
        String dirnameBuiltin = modulePath.getParent() == null ? "." : modulePath.getParent().getAbsoluteFile().normalize().toString();
        JSObject exportsBuiltin = createExportsBuiltin(realm);
        JSObject moduleBuiltin = createModuleBuiltin(realm, exportsBuiltin, filenameBuiltin);
        JSObject requireBuiltin = createRequireBuiltin(realm, moduleBuiltin, filenameBuiltin);
        JSObject env = JSOrdinary.create(getContext(), getRealm());
        JSObject.set(env, Strings.ENV_PROPERTY_NAME, JSOrdinary.create(getContext(), getRealm()));
        // Parse the module
        Object moduleExecutableFunction = parseModule(realm, source);
        // Execute the module.
        if (JSFunction.isJSFunction(moduleExecutableFunction)) {
            log("adding to cache ", normalizedPath);
            commonJSCache.put(normalizedPath, moduleBuiltin);
            try {
                debugStackPush(moduleIdentifier);
                log("executing '", filenameBuiltin, "' for ", moduleIdentifier);
                JSFunction.call(JSArguments.create(moduleExecutableFunction, moduleExecutableFunction, exportsBuiltin, requireBuiltin, moduleBuiltin, filenameBuiltin,
                                Strings.fromJavaString(dirnameBuiltin), env));
                JSObject.set(moduleBuiltin, Strings.LOADED_PROPERTY_NAME, true);
                return JSObject.get(moduleBuiltin, Strings.EXPORTS_PROPERTY_NAME);
            } catch (Exception e) {
                log("EXCEPTION: '", e.getMessage(), "'");
                throw e;
            } finally {
                debugStackPop();
                Object module = JSObject.get(moduleBuiltin, Strings.EXPORTS_PROPERTY_NAME);
                log("done '", moduleIdentifier, "' module.exports: ", module, module);
            }
        }
        return null;
    }

    private static Object parseModule(JSRealm realm, Source source) {
        JSContext context = realm.getContext();
        String body = source.getCharacters() + "\n";
        // Will throw a JS error (if syntax is wrong).
        context.getEvaluator().checkFunctionSyntax(context, context.getParserOptions(), MODULE_FUNCTION_ARGS, body, false, false, source.getPath());
        CharSequence characters = MODULE_PREAMBLE_PREFIX + MODULE_FUNCTION_ARGS + MODULE_PREAMBLE_POST + body + MODULE_END;
        Source moduleSources = Source.newBuilder(source).content(characters).build();
        CallTarget moduleCallTarget = realm.getEnv().parsePublic(moduleSources);
        return moduleCallTarget.call();
    }

    private JSDynamicObject evalJsonFile(TruffleFile jsonFile) {
        try {
            if (fileExists(jsonFile)) {
                Source source;
                JSRealm realm = getRealm();
                TruffleFile file = GlobalBuiltins.resolveRelativeFilePath(jsonFile.toString(), realm.getEnv());
                if (file.isRegularFile()) {
                    source = sourceFromTruffleFile(file);
                } else {
                    throw fail(jsonFile.toString());
                }
                JSFunctionObject parse = (JSFunctionObject) realm.getJsonParseFunctionObject();
                assert source != null;
                TruffleString jsonString = Strings.fromJavaString(source.getCharacters().toString());
                Object jsonObj = JSFunction.call(JSArguments.create(Undefined.instance, parse, jsonString));
                if (JSDynamicObject.isJSDynamicObject(jsonObj)) {
                    return (JSDynamicObject) jsonObj;
                }
            }
            throw fail(jsonFile.toString());
        } catch (SecurityException | UnsupportedOperationException | IllegalArgumentException e) {
            throw Errors.createErrorFromException(e);
        }
    }

    static JSException fail(String moduleIdentifier) {
        return JSException.create(JSErrorType.TypeError, "Cannot load module: '" + moduleIdentifier + "'");
    }

    private static JSException fail(String moduleIdentifier, String extraMessage) {
        return JSException.create(JSErrorType.TypeError, "Cannot load module: '" + moduleIdentifier + "': " + extraMessage);
    }

    private static JSObject createModuleBuiltin(JSRealm realm, JSDynamicObject exportsBuiltin, TruffleString fileNameBuiltin) {
        JSObject module = JSOrdinary.create(realm.getContext(), realm);
        JSObject.set(module, Strings.EXPORTS_PROPERTY_NAME, exportsBuiltin);
        JSObject.set(module, Strings.ID_PROPERTY_NAME, fileNameBuiltin);
        JSObject.set(module, Strings.FILENAME_PROPERTY_NAME, fileNameBuiltin);
        JSObject.set(module, Strings.LOADED_PROPERTY_NAME, false);
        return module;
    }

    private static JSObject createRequireBuiltin(JSRealm realm, JSDynamicObject moduleBuiltin, TruffleString fileNameBuiltin) {
        JSFunctionObject mainRequire = (JSFunctionObject) realm.getCommonJSRequireFunctionObject();
        Object mainResolve = JSObject.get(mainRequire, Strings.RESOLVE_PROPERTY_NAME);
        JSFunctionData functionData = JSFunction.getFunctionData(mainRequire);
        JSObject newRequire = JSFunction.create(realm, functionData);
        JSObject.set(newRequire, Strings.MODULE_PROPERTY_NAME, moduleBuiltin);
        JSObject.set(newRequire, Strings.RESOLVE_PROPERTY_NAME, mainResolve);
        // XXX(db) Here, we store the current filename in the (new) require builtin.
        // In this way, we avoid managing a shadow stack to track the current require's parent.
        // In Node.js, this is done using a (closed) level variable.
        JSObject.set(newRequire, Strings.FILENAME_VAR_NAME, fileNameBuiltin);
        return newRequire;
    }

    private static JSObject 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 isCjsFile(TruffleFile maybeModule) {
        return hasExtension(Objects.requireNonNull(maybeModule.getName()), CJS_EXT);
    }

    private static boolean isMjsFile(TruffleFile maybeModule) {
        return hasExtension(Objects.requireNonNull(maybeModule.getName()), MJS_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 static TruffleFile getModuleResolutionEntryPath(JSDynamicObject currentRequire, JSRealm realm, TruffleLanguage.Env env) {
        if (JSDynamicObject.isJSDynamicObject(currentRequire)) {
            Object maybeFilename = JSObject.get(currentRequire, Strings.FILENAME_VAR_NAME);
            if (maybeFilename instanceof TruffleString str) {
                String fileName = Strings.toJavaString(str);
                if (isFile(env, fileName)) {
                    TruffleFile maybeParent = getParent(env, fileName);
                    if (maybeParent != null) {
                        return maybeParent;
                    }
                }
            }
            // dirname not a string. Use default cwd.
        }
        // This is not a nested `require()` call, so we use the default cwd.
        return getModuleResolveCurrentWorkingDirectory(realm, env);
    }

    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.endsWith(ext);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy