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

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

/*
 * Copyright (c) 2019, 2020, 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 java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.frame.FrameInstance;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.js.builtins.GlobalBuiltins;
import com.oracle.truffle.js.lang.JavaScriptLanguage;
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.JSRealm;
import com.oracle.truffle.js.runtime.JSRuntime;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
import com.oracle.truffle.js.runtime.objects.JSObject;

final class CommonJSResolution {

    private static final String JS_EXT = ".js";
    private static final String JSON_EXT = ".json";
    private static final String NODE_EXT = ".node";
    public static final String INDEX_JS = "index.js";
    private static final String INDEX_JSON = "index.json";
    private static final String INDEX_NODE = "index.node";
    public static final String PACKAGE_JSON = "package.json";
    private static final String NODE_MODULES = "node_modules";
    public static final String PACKAGE_JSON_MAIN_PROPERTY_NAME = "main";
    public static final String PACKAGE_JSON_TYPE_PROPERTY_NAME = "type";
    public static final String PACKAGE_JSON_MODULE_VALUE = "module";

    private static final String[] CORE_MODULES = new String[]{"assert", "async_hooks", "buffer", "child_process", "cluster", "crypto",
                    "dgram", "dns", "domain", "events", "fs", "http", "http2", "https", "module", "net",
                    "os", "path", "perf_hooks", "punycode", "querystring", "readline", "repl",
                    "stream", "string_decoder", "tls", "trace_events", "tty", "url", "util",
                    "v8", "vm", "worker_threads", "zlib"};

    private CommonJSResolution() {
    }

    static boolean isCoreModule(String moduleIdentifier) {
        return Arrays.asList(CORE_MODULES).contains(moduleIdentifier);
    }

    static String getCurrentFileNameFromStack() {
        FrameInstance callerFrame = Truffle.getRuntime().getCallerFrame();
        if (callerFrame != null) {
            SourceSection encapsulatingSourceSection = null;
            if (callerFrame.getCallNode() != null) {
                encapsulatingSourceSection = callerFrame.getCallNode().getEncapsulatingSourceSection();
            } else {
                RootNode frameRootNode = JSFunction.getFrameRootNode(callerFrame);
                if (frameRootNode != null) {
                    encapsulatingSourceSection = frameRootNode.getEncapsulatingSourceSection();
                }
            }
            if (encapsulatingSourceSection != null && encapsulatingSourceSection.getSource() != null) {
                Source source = encapsulatingSourceSection.getSource();
                return source.getPath();
            }
        }
        return null;
    }

    /**
     * CommonJS `require` implementation based on the Node.js runtime loading mechanism as described
     * in the Node.js documentation: https://nodejs.org/api/modules.html.
     *
     * @formatter:off
     *       1. If X is a core module
     *           a. return the core module
     *           b. STOP
     *
     *       2. If X begins with '/'
     *           a. set Y to be the filesystem root
     *
     *       3. If X begins with './' or '/' or '../'
     *           a. LOAD_AS_FILE(Y + X)
     *           b. LOAD_AS_DIRECTORY(Y + X)
     *           c. THROW "not found"
     *
     *       4. LOAD_NODE_MODULES(X, dirname(Y))
     *       5. LOAD_SELF_REFERENCE(X, dirname(Y))
     *       6. THROW "not found"
     *@formatter:on
     *
     * @param context A valid JS Context.
     * @param moduleIdentifier The module identifier to be resolved.
     * @param entryPath The initial directory from which the resolution algorithm is executed.
     * @return A {@link TruffleFile} for the module executable file, or {@code null} if the module cannot be resolved.
     */
    @CompilerDirectives.TruffleBoundary
    static TruffleFile resolve(JSContext context, String moduleIdentifier, TruffleFile entryPath) {
        // 1. If X is an empty module
        if ("".equals(moduleIdentifier)) {
            return null;
        }
        TruffleLanguage.Env env = context.getRealm().getEnv();
        // 2. If X begins with '/'
        TruffleFile currentWorkingPath = entryPath;
        if (moduleIdentifier.charAt(0) == '/') {
            currentWorkingPath = getFileSystemRootPath(env);
        }
        // 3. If X begins with './' or '/' or '../'
        if (isPathFileName(moduleIdentifier)) {
            TruffleFile module = loadAsFileOrDirectory(context, env, joinPaths(env, currentWorkingPath, moduleIdentifier));
            // XXX(db) The Node.js informal spec says we should throw if module is null here.
            // Node v12.x, however, does not throw and attempts to load as a folder.
            if (module != null) {
                return module;
            }
        }
        // 4. 5. 6. Try loading as a folder, or throw if not existing
        return loadNodeModulesOrSelfReference(context, env, moduleIdentifier, currentWorkingPath);
    }

    private static TruffleFile loadNodeModulesOrSelfReference(JSContext cx, TruffleLanguage.Env env, String moduleIdentifier, TruffleFile startFolder) {
        /* @formatter:off
         *
         * 1. let DIRS = NODE_MODULES_PATHS(START)
         * 2. for each DIR in DIRS:
         *     a. LOAD_AS_FILE(DIR/X)
         *     b. LOAD_AS_DIRECTORY(DIR/X)
         *
         * @formatter:on
         */
        List nodeModulesPaths = getNodeModulesPaths(startFolder);
        for (TruffleFile s : nodeModulesPaths) {
            TruffleFile module = loadAsFileOrDirectory(cx, env, joinPaths(env, s, moduleIdentifier));
            if (module != null) {
                return module;
            }
        }
        return null;
    }

    public static TruffleFile loadIndex(TruffleLanguage.Env env, TruffleFile modulePath) {
        /* @formatter:off
         *
         * LOAD_INDEX(X)
         *     1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
         *     2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
         *     3. If X/index.node is a file, load X/index.node as binary addon. STOP
         *
         * @formatter:on
         */
        TruffleFile indexJs = joinPaths(env, modulePath, INDEX_JS);
        if (fileExists(indexJs)) {
            return indexJs;
        }
        TruffleFile indexJson = joinPaths(env, modulePath, INDEX_JSON);
        if (fileExists(indexJson)) {
            return indexJson;
        } else if (fileExists(joinPaths(env, modulePath, INDEX_NODE))) {
            // Ignore .node files.
            return null;
        }
        return null;
    }

    static TruffleFile loadAsFile(TruffleLanguage.Env env, TruffleFile modulePath) {
        /* @formatter:off
         *
         * LOAD_AS_FILE(X)
         *     1. If X is a file, load X as JavaScript text. STOP
         *     2. If X.js is a file, load X.js as JavaScript text. STOP
         *     3. If X.json is a file, parse X.json to a JavaScript Object. STOP
         *     4. If X.node is a file, load X.node as binary addon. STOP
         *
         * @formatter:on
         */
        if (fileExists(modulePath)) {
            return modulePath;
        }
        TruffleFile moduleJs = env.getPublicTruffleFile(modulePath.toString() + JS_EXT);
        if (fileExists(moduleJs)) {
            return moduleJs;
        }
        TruffleFile moduleJson = env.getPublicTruffleFile(modulePath.toString() + JSON_EXT);
        if (fileExists(moduleJson)) {
            return moduleJson;
        }
        if (fileExists(env.getPublicTruffleFile(modulePath.toString() + NODE_EXT))) {
            // .node files not supported.
            return null;
        }
        return null;
    }

    public static List getNodeModulesPaths(TruffleFile path) {
        List list = new ArrayList<>();
        List paths = getAllParentPaths(path);
        for (TruffleFile p : paths) {
            if (p.endsWith(NODE_MODULES)) {
                list.add(p);
            } else {
                TruffleFile truffleFile = p.resolve(NODE_MODULES);
                list.add(truffleFile);
            }
        }
        return list;
    }

    private static TruffleFile loadAsFileOrDirectory(JSContext cx, TruffleLanguage.Env env, TruffleFile modulePath) {
        TruffleFile maybeFile = loadAsFile(env, modulePath);
        if (maybeFile == null) {
            return loadAsDirectory(cx, env, modulePath);
        } else {
            return maybeFile;
        }
    }

    private static List getAllParentPaths(TruffleFile from) {
        List paths = new ArrayList<>();
        TruffleFile p = from;
        while (p != null) {
            paths.add(p);
            p = p.getParent();
        }
        return paths;
    }

    private static TruffleFile loadAsDirectory(JSContext cx, TruffleLanguage.Env env, TruffleFile modulePath) {
        TruffleFile packageJson = joinPaths(env, modulePath, PACKAGE_JSON);
        if (fileExists(packageJson)) {
            DynamicObject jsonObj = loadJsonObject(packageJson, cx);
            if (JSDynamicObject.isJSDynamicObject(jsonObj)) {
                Object main = JSObject.get(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME);
                if (!JSRuntime.isString(main)) {
                    return loadIndex(env, modulePath);
                }
                TruffleFile module = joinPaths(env, modulePath, JSRuntime.safeToString(main));
                TruffleFile asFile = loadAsFile(env, module);
                if (asFile != null) {
                    return asFile;
                } else {
                    return loadIndex(env, module);
                }
            }
        } else {
            return loadIndex(env, modulePath);
        }
        return null;
    }

    public static DynamicObject loadJsonObject(TruffleFile jsonFile, JSContext context) {
        try {
            if (fileExists(jsonFile)) {
                Source source = null;
                JSRealm realm = context.getRealm();
                TruffleFile file = GlobalBuiltins.resolveRelativeFilePath(jsonFile.toString(), realm.getEnv());
                if (file.isRegularFile()) {
                    source = sourceFromTruffleFile(file);
                }
                if (source == null) {
                    return null;
                }
                DynamicObject parse = (DynamicObject) realm.getJsonParseFunctionObject();
                String jsonString = source.getCharacters().toString();
                Object jsonObj = JSFunction.call(JSArguments.create(parse, parse, jsonString));
                if (JSDynamicObject.isJSDynamicObject(jsonObj)) {
                    return (DynamicObject) jsonObj;
                }
            }
            return null;
        } catch (SecurityException e) {
            throw Errors.createErrorFromException(e);
        }
    }

    private static Source sourceFromTruffleFile(TruffleFile file) {
        try {
            return Source.newBuilder(JavaScriptLanguage.ID, file).build();
        } catch (IOException | SecurityException e) {
            return null;
        }
    }

    public static boolean fileExists(TruffleFile modulePath) {
        return modulePath.exists() && modulePath.isRegularFile();
    }

    private static boolean isPathFileName(String moduleIdentifier) {
        return moduleIdentifier.startsWith("/") || moduleIdentifier.startsWith("./") || moduleIdentifier.startsWith("../");
    }

    public static TruffleFile joinPaths(TruffleLanguage.Env env, TruffleFile p1, String p2) {
        Objects.requireNonNull(p1);
        String pathSeparator = env.getFileNameSeparator();
        String pathName = p1.normalize().toString();
        TruffleFile truffleFile = env.getPublicTruffleFile(pathName + pathSeparator + p2);
        return truffleFile.normalize();
    }

    private static TruffleFile getFileSystemRootPath(TruffleLanguage.Env env) {
        TruffleFile root = env.getCurrentWorkingDirectory();
        TruffleFile last = root;
        while (root != null) {
            last = root;
            root = root.getParent();
        }
        return last;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy