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

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

/*
 * Copyright (c) 2020, 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.CommonJSRequireBuiltin.log;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.CJS_EXT;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.FILE;
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_MODULES;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.PACKAGE_JSON;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.getCoreModuleReplacement;
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.joinPaths;
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.runtime.Strings.EXPORTS_PROPERTY_NAME;
import static com.oracle.truffle.js.runtime.Strings.MODULE;
import static com.oracle.truffle.js.runtime.Strings.NAME;
import static com.oracle.truffle.js.runtime.Strings.PACKAGE_JSON_MAIN_PROPERTY_NAME;
import static com.oracle.truffle.js.runtime.Strings.TYPE;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

import com.oracle.js.parser.ir.Module.ModuleRequest;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.strings.TruffleString;
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.Strings;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.builtins.JSFunctionObject;
import com.oracle.truffle.js.runtime.objects.DefaultESModuleLoader;
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
import com.oracle.truffle.js.runtime.objects.JSModuleData;
import com.oracle.truffle.js.runtime.objects.JSModuleRecord;
import com.oracle.truffle.js.runtime.objects.JSObject;
import com.oracle.truffle.js.runtime.objects.Null;
import com.oracle.truffle.js.runtime.objects.ScriptOrModule;
import com.oracle.truffle.js.runtime.objects.Undefined;

public final class NpmCompatibleESModuleLoader extends DefaultESModuleLoader {

    private static final URI TryCommonJS = URI.create("custom:///try-common-js-token");
    private static final URI TryCustomESM = URI.create("custom:///try-custom-esm-token");

    private static final String MODULE_NOT_FOUND = "Module not found: '";
    private static final String UNSUPPORTED_JSON = "JSON packages not supported.";
    private static final String FAILED_BUILTIN = "Failed to load built-in ES module: '";
    private static final String INVALID_MODULE_SPECIFIER = "Invalid module specifier: '";
    private static final String UNSUPPORTED_FILE_EXTENSION = "Unsupported file extension: '";
    private static final String UNSUPPORTED_PACKAGE_EXPORTS = "Unsupported package exports: '";
    private static final String UNSUPPORTED_PACKAGE_IMPORTS = "Unsupported package imports: '";
    private static final String UNSUPPORTED_DIRECTORY_IMPORT = "Unsupported directory import: '";
    private static final String INVALID_PACKAGE_CONFIGURATION = "Invalid package configuration: '";

    public static NpmCompatibleESModuleLoader create(JSRealm realm) {
        return new NpmCompatibleESModuleLoader(realm);
    }

    private NpmCompatibleESModuleLoader(JSRealm realm) {
        super(realm);
    }

    /**
     * Node.js-compatible implementation of ES modules loading.
     *
     * @see ES Modules
     * @see Resolver algorithm
     *
     * @param referencingModule Referencing ES Module.
     * @param moduleRequest ES Modules Request.
     * @return ES Module record for this module.
     */
    @TruffleBoundary
    @Override
    public JSModuleRecord resolveImportedModule(ScriptOrModule referencingModule, ModuleRequest moduleRequest) {
        String specifier = moduleRequest.getSpecifier().toJavaStringUncached();
        log("IMPORT resolve ", specifier);
        String moduleReplacementName = getCoreModuleReplacement(realm, specifier);
        if (moduleReplacementName != null) {
            return loadCoreModuleReplacement(referencingModule, moduleRequest, moduleReplacementName);
        }
        try {
            TruffleLanguage.Env env = realm.getEnv();
            URI parentURL = getFullPath(referencingModule).toUri();
            URI resolution = esmResolve(specifier, parentURL, env);
            if (resolution == TryCommonJS) {
                // Compatibility mode: try loading as a CommonJS module.
                return tryLoadingAsCommonjsModule(specifier);
            } else {
                if (resolution == TryCustomESM) {
                    // Failed ESM resolution. Give the virtual FS a chance to map to a file.
                    // A custom Truffle FS might still try to map a package specifier to some file.
                    TruffleFile maybeFile = env.getPublicTruffleFile(specifier);
                    if (maybeFile.exists() && !maybeFile.isDirectory()) {
                        return loadModuleFromUrl(referencingModule, moduleRequest, maybeFile, maybeFile.getPath());
                    }
                } else if (resolution != null) {
                    TruffleFile file = env.getPublicTruffleFile(resolution);
                    return loadModuleFromUrl(referencingModule, moduleRequest, file, file.getPath());
                }
            }
            // Really could not load as ESM.
            throw fail(MODULE_NOT_FOUND, specifier);
        } catch (IOException | SecurityException | UnsupportedOperationException | IllegalArgumentException e) {
            log("IMPORT resolve ", specifier, " FAILED ", e.getMessage());
            throw Errors.createErrorFromException(e);
        }
    }

    private JSModuleRecord loadCoreModuleReplacement(ScriptOrModule referencingModule, ModuleRequest moduleRequest, String moduleReplacementName) {
        String specifier = moduleRequest.getSpecifier().toJavaStringUncached();
        log("IMPORT resolve built-in ", specifier);
        JSModuleRecord existingModule = moduleMap.get(specifier);
        if (existingModule != null) {
            log("IMPORT resolve built-in from cache ", specifier);
            return existingModule;
        }
        Source src;
        if (moduleReplacementName.endsWith(MJS_EXT)) {
            URI maybeUri = asURI(moduleReplacementName);
            if (maybeUri != null) {
                // Load from URI
                TruffleLanguage.Env env = realm.getEnv();
                URI parentURL = getFullPath(referencingModule).toUri();
                URI resolution = esmResolve(moduleReplacementName, parentURL, env);
                assert resolution != null;
                try {
                    TruffleFile file = env.getPublicTruffleFile(resolution);
                    return loadModuleFromUrl(referencingModule, moduleRequest, file, file.getPath());
                } catch (IOException e) {
                    throw fail(FAILED_BUILTIN, specifier);
                }
            } else {
                // Just load the module
                try {
                    String cwdOption = realm.getContextOptions().getRequireCwd();
                    TruffleFile cwd = cwdOption.isEmpty() ? realm.getEnv().getCurrentWorkingDirectory() : realm.getEnv().getPublicTruffleFile(cwdOption);
                    TruffleFile modulePath = joinPaths(cwd, moduleReplacementName);
                    src = Source.newBuilder(ID, modulePath).build();
                } catch (IOException | SecurityException e) {
                    throw fail(FAILED_BUILTIN, specifier);
                }
            }
        } else {
            // Else, try loading as commonjs built-in module replacement
            return tryLoadingAsCommonjsModule(specifier);
        }
        JSModuleData parsedModule = realm.getContext().getEvaluator().envParseModule(realm, src);
        JSModuleRecord record = new JSModuleRecord(parsedModule, this);
        moduleMap.put(specifier, record);
        return record;
    }

    private JSModuleRecord tryLoadingAsCommonjsModule(String specifier) {
        JSModuleRecord existingModule = moduleMap.get(specifier);
        if (existingModule != null) {
            log("IMPORT resolve built-in from cache ", specifier);
            return existingModule;
        }
        JSFunctionObject require = (JSFunctionObject) realm.getCommonJSRequireFunctionObject();
        // Any exception thrown during module loading will be propagated
        Object maybeModule = JSFunction.call(JSArguments.create(Undefined.instance, require, Strings.fromJavaString(specifier)));
        if (maybeModule == Undefined.instance || !JSDynamicObject.isJSDynamicObject(maybeModule)) {
            throw fail(FAILED_BUILTIN, specifier);
        }
        JSDynamicObject module = (JSDynamicObject) maybeModule;
        // Wrap any exported symbol in an ES module.
        List exportedValues = JSObject.enumerableOwnNames(module);
        var moduleBody = Strings.builderCreate();
        Strings.builderAppend(moduleBody, "const builtinModule = require('");
        Strings.builderAppend(moduleBody, specifier);
        Strings.builderAppend(moduleBody, "');\n");
        for (TruffleString s : exportedValues) {
            Strings.builderAppend(moduleBody, "export const ");
            Strings.builderAppend(moduleBody, s);
            Strings.builderAppend(moduleBody, " = builtinModule.");
            Strings.builderAppend(moduleBody, s);
            Strings.builderAppend(moduleBody, ";\n");
        }
        Strings.builderAppend(moduleBody, "export default builtinModule;");
        Source src = Source.newBuilder(ID, Strings.builderToJavaString(moduleBody), specifier + "-internal.mjs").build();
        JSModuleData parsedModule = realm.getContext().getEvaluator().envParseModule(realm, src);
        JSModuleRecord record = new JSModuleRecord(parsedModule, this);
        moduleMap.put(specifier, record);
        return record;
    }

    //
    // #### ESM resolution algorithm emulation.
    //
    // Best-effort implementation based on Node.js' v16.15.0 resolution algorithm.
    //

    /**
     * ESM_RESOLVE(specifier, parentURL).
     */
    private URI esmResolve(String specifier, URI parentURL, TruffleLanguage.Env env) {
        // 1. Let resolved be undefined.
        URI resolved = asURI(specifier);
        // 2. If specifier is a valid URL, then
        // 2.1 Set resolved to the result of parsing and reserializing specifier as a URL.
        if (resolved == null) {
            if (!specifier.isEmpty() && specifier.charAt(0) == '/' || isRelativePathFileName(specifier)) {
                // 3. Otherwise, if specifier starts with "/", "./" or "../", then
                // 3.1 Set resolved to the URL resolution of specifier relative to parentURL.
                resolved = resolveRelativeToParent(specifier, parentURL);
            } else if (!specifier.isEmpty() && specifier.charAt(0) == '#') {
                // 4. Otherwise, if specifier starts with "#", then
                throw fail(UNSUPPORTED_PACKAGE_IMPORTS, specifier);
            } else {
                // 5.1 Note: specifier is now a bare specifier.
                // 5.2 Set resolvedURL the result of PACKAGE_RESOLVE(specifier, parentURL).
                resolved = packageResolve(specifier, parentURL, env);
            }
        }
        if (resolved == null) {
            // package was not found: will try loading as CommonJS module instead
            return TryCommonJS;
        } else if (resolved == TryCommonJS || resolved == TryCustomESM) {
            // Try customFS lookup
            return resolved;
        }
        // 6. Let format be undefined.
        Format format;
        // 7. If resolved is a "file:" URL, then
        if (isFileURI(resolved)) {
            // 7.1 If resolvedURL contains any percent encodings of "/" or "\" ("%2f" and "%5C"
            // respectively), then
            if (resolved.toString().toUpperCase().contains("%2F") || resolved.toString().toUpperCase().contains("%5C")) {
                // 7.1.1 Throw an Invalid Module Specifier error.
                throw fail(INVALID_MODULE_SPECIFIER, specifier);
            }
            // 7.2 If the file at resolved is a directory, then
            if (isDirectory(resolved, env)) {
                // 7.2.1 Throw an Unsupported Directory Import error.
                throw fail(UNSUPPORTED_DIRECTORY_IMPORT, specifier);
            }
            // 7.3 If the file at resolved does not exist, then
            if (!fileExists(resolved, env)) {
                // 7.3.1 Throw a Module Not Found error.
                throw fail(MODULE_NOT_FOUND, specifier);
            }
            // 7.4 Set resolved to the real path of resolved, maintaining the same URL querystring
            // and fragment components.
            resolved = resolved.normalize();
            // 7.5 Set format to the result of ESM_FILE_FORMAT(resolved).
            format = esmFileFormat(resolved, env);
        } else {
            // 8. Otherwise
            // 8.1 Set format the module format of the content type associated with the URL
            // resolved.
            format = getAssociatedDefaultFormat(resolved);
        }
        if (format == Format.CommonJS) {
            // Will load as CommonJS.
            return TryCommonJS;
        } else {
            // Will load as ESM.
            return resolved;
        }
    }

    /**
     * ESM_FILE_FORMAT(url).
     */
    private Format esmFileFormat(URI url, TruffleLanguage.Env env) {
        // 1. Assert: url corresponds to an existing file.
        assert fileExists(url, env);
        // 2. If url ends in ".mjs", Return "module".
        if (url.getPath().endsWith(MJS_EXT)) {
            return Format.ESM;
        }
        // 3. If url ends in ".cjs", Return "module".
        if (url.getPath().endsWith(CJS_EXT)) {
            // Note: we will try loading as CJS like Node.js does.
            return Format.CommonJS;
        }
        // 4. If url ends in ".mjs", Return "module".
        if (url.getPath().endsWith(JSON_EXT)) {
            throw failMessage(UNSUPPORTED_JSON);
        }
        // 5. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url).
        URI packageUri = lookupPackageScope(url, env);
        if (packageUri != null) {
            // 6. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
            PackageJson pjson = readPackageJson(packageUri, env);
            // 7. If pjson?.type exists and is "module", then
            if (pjson != null && pjson.hasTypeModule()) {
                // 7.1 If url ends in ".js", then Return "module"
                if (url.getPath().endsWith(JS_EXT)) {
                    return Format.ESM;
                }
            }
        } else if (url.getPath().endsWith(JS_EXT)) {
            // Np Package.json with .js extension: try loading as CJS like Node.js does.
            return Format.CommonJS;
        }
        // 8. Otherwise, Throw an Unsupported File Extension error.
        throw fail(UNSUPPORTED_FILE_EXTENSION, url.toString());
    }

    /**
     * PACKAGE_RESOLVE(packageSpecifier, parentURL).
     */
    private URI packageResolve(String packageSpecifier, URI parentURL, TruffleLanguage.Env env) {
        // 1. Let packageName be undefined.
        String packageName;
        // 2. If packageSpecifier is an empty string, then
        if (packageSpecifier.isEmpty()) {
            // Throw an Invalid Module Specifier error.
            throw fail(INVALID_MODULE_SPECIFIER, packageSpecifier);
        }
        // 3. Note: we ignore Node.js builtin module names.
        int packageSpecifierSeparator = packageSpecifier.indexOf('/');
        // 4. If packageSpecifier does not start with "@", then
        if (packageSpecifier.charAt(0) != '@') {
            // Set packageName to the substring of packageSpecifier until the first "/"
            if (packageSpecifierSeparator != -1) {
                packageName = packageSpecifier.substring(0, packageSpecifierSeparator);
            } else {
                // or the end of the string.
                packageName = packageSpecifier;
            }
        } else {
            // 5. Otherwise, if packageSpecifier does not contain a "/" separator, then
            if (packageSpecifierSeparator == -1) {
                // Throw an Invalid Module Specifier error.
                throw fail(INVALID_MODULE_SPECIFIER, packageSpecifier);
            }
            // Set packageName to the substring of packageSpecifier until the second "/" separator
            int secondSeparator = packageSpecifier.indexOf('/', packageSpecifierSeparator + 1);
            if (secondSeparator != -1) {
                packageName = packageSpecifier.substring(0, secondSeparator);
            } else {
                // or the end of the string.
                packageName = packageSpecifier;
            }
        }
        // 6. If packageName starts with "." or contains "\" or "%", then
        if (packageName.charAt(0) == '.' || packageName.indexOf('\\') >= 0 || packageName.indexOf('%') >= 0) {
            // Throw an Invalid Module Specifier error.
            throw fail(INVALID_MODULE_SPECIFIER, packageSpecifier);
        }
        // 7. Let packageSubpath be "." concatenated with the substring of packageSpecifier from the
        // position at the length of packageName.
        String packageSpecifierSub = packageSpecifier.substring(packageName.length());
        String packageSubpath = DOT + packageSpecifierSub;
        // 8. If packageSubpath ends in "/", then
        if (packageSubpath.endsWith(SLASH)) {
            // Throw an Invalid Module Specifier error.
            throw fail(INVALID_MODULE_SPECIFIER, packageSpecifier);
        }
        // 9. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath,
        // parentURL).
        URI selfUrl = packageSelfResolve(packageName, parentURL, env);
        // 10. If selfUrl is not undefined, return selfUrl.
        if (selfUrl != null) {
            return selfUrl;
        }
        TruffleFile currentParentUrl = env.getPublicTruffleFile(parentURL);
        // 11. While parentURL is not the file system root,
        while (currentParentUrl != null && !isRoot(currentParentUrl)) {
            // 11.1 Let packageURL be the URL resolution of "node_modules/" concatenated with
            // packageSpecifier, relative to parentURL.
            URI packageUrl = getPackageUrl(packageName, currentParentUrl);
            // 11.2 Set parentURL to the parent folder URL of parentURL.
            currentParentUrl = currentParentUrl.getParent();
            // 11.3 If the folder at packageURL does not exist, then
            TruffleFile maybeFolder = packageUrl != null ? env.getPublicTruffleFile(packageUrl) : null;
            if (maybeFolder == null || !maybeFolder.exists() || !maybeFolder.isDirectory()) {
                continue;
            }
            // 11.4 Let pjson be the result of READ_PACKAGE_JSON(packageURL).
            PackageJson pjson = readPackageJson(packageUrl, env);
            // 11.5 If pjson is not null and pjson.exports is not null or undefined, then
            if (pjson != null && pjson.hasExportsProperty()) {
                throw fail(UNSUPPORTED_PACKAGE_EXPORTS, packageSpecifier);
            } else if (packageSubpath.equals(DOT)) {
                // 11.6 Otherwise, if packageSubpath is equal to ".", then
                // 11.6.1 If pjson.main is a string, then return the URL resolution of main in
                // packageURL.
                if (pjson != null && pjson.hasMainProperty()) {
                    TruffleString main = pjson.getMainProperty();
                    return packageUrl.resolve(main.toString());
                } else {
                    // For backwards compatibility: return null and try loading as a legacy CJS.
                    // https://github.com/oracle/graaljs/blob/master/graal-nodejs/lib/internal/modules/esm/resolve.js#L918
                    return TryCommonJS;
                }
            }
            // 7. Otherwise, Return the URL resolution of packageSubpath in packageURL.
            return packageUrl.resolve(packageSubpath);
        }
        // 12. Will Throw a Module Not Found error.
        return TryCustomESM;
    }

    private static boolean isRoot(TruffleFile file) {
        if (file.isDirectory() && file.isAbsolute()) {
            return file.getParent() == null;
        }
        return false;
    }

    /**
     * PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
     */
    private URI packageSelfResolve(String packageName, URI parentURL, TruffleLanguage.Env env) {
        // 1. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
        URI packageUrl = lookupPackageScope(parentURL, env);
        // 2. If packageURL is null, then Return undefined.
        if (packageUrl == null) {
            return null;
        }
        // 3. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
        PackageJson pjson = readPackageJson(packageUrl, env);
        // 4. If pjson is null or if pjson.exports is null or undefined, Return undefined.
        if (pjson == null || !pjson.hasExportsProperty()) {
            return null;
        }
        // 5. If pjson.name is equal to packageName, then
        if (pjson.namePropertyEquals(packageName)) {
            throw failMessage(UNSUPPORTED_PACKAGE_EXPORTS);
        }
        // 6. Otherwise, return undefined.
        return null;
    }

    /**
     * LOOKUP_PACKAGE_SCOPE(url).
     */
    private URI lookupPackageScope(URI url, TruffleLanguage.Env env) {
        // 1. Let scopeURL be url
        URI scopeUrl = url;
        // 2. While scopeURL is not the file system root,
        while (scopeUrl != null) {
            // 2.1. Set scopeURL to the parent URL of scopeURL.
            scopeUrl = getParentUrl(scopeUrl, env);
            if (scopeUrl == null) {
                break;
            }
            // 2.2 If scopeURL ends in a "node_modules" path segment, return null.
            if (scopeUrl.toString().endsWith(NODE_MODULES)) {
                return null;
            }
            // 2.3 Let pjsonURL be the resolution of "package.json" within scopeURL.
            // 2.4 if the file at pjsonURL exists, then Return scopeURL
            if (readPackageJson(scopeUrl, env) != null) {
                return scopeUrl;
            }
        }
        // 3. Return null.
        return null;
    }

    //
    // ##### Utils
    //

    private enum Format {
        CommonJS,
        ESM
    }

    private static class PackageJson {

        private final JSDynamicObject jsonObj;

        PackageJson(JSDynamicObject jsonObj) {
            assert jsonObj != null;
            assert JSObject.isJSObject(jsonObj);
            this.jsonObj = jsonObj;
        }

        boolean hasTypeModule() {
            if (hasNonNullProperty(jsonObj, TYPE)) {
                Object nameValue = JSObject.get(jsonObj, TYPE);
                if (nameValue instanceof TruffleString nameStr) {
                    return Strings.equals(MODULE, nameStr);
                }
            }
            return false;
        }

        private static boolean hasNonNullProperty(JSDynamicObject object, TruffleString keyName) {
            if (JSObject.hasProperty(object, keyName)) {
                Object value = JSObject.get(object, keyName);
                return value != Null.instance && value != Undefined.instance;
            }
            return false;
        }

        public boolean hasExportsProperty() {
            return hasNonNullProperty(jsonObj, EXPORTS_PROPERTY_NAME);
        }

        public boolean hasMainProperty() {
            if (JSObject.hasProperty(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME)) {
                Object value = JSObject.get(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME);
                return Strings.isTString(value);
            }
            return false;
        }

        public TruffleString getMainProperty() {
            assert hasMainProperty();
            Object value = JSObject.get(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME);
            return (TruffleString) value;
        }

        public boolean namePropertyEquals(String name) {
            TruffleString packageName = Strings.fromJavaString(name);
            if (hasNonNullProperty(jsonObj, NAME)) {
                Object nameValue = JSObject.get(jsonObj, NAME);
                if (nameValue instanceof TruffleString nameStr) {
                    return Strings.equals(packageName, nameStr);
                }
            }
            return false;
        }
    }

    private PackageJson readPackageJson(URI packageUrl, TruffleLanguage.Env env) {
        URI pjsonUrl = packageUrl.resolve(PACKAGE_JSON);
        if (!fileExists(pjsonUrl, env)) {
            return null;
        }
        JSDynamicObject jsonObj = loadJsonObject(env.getPublicTruffleFile(pjsonUrl), realm);
        if (!JSDynamicObject.isJSDynamicObject(jsonObj)) {
            throw failMessage(INVALID_PACKAGE_CONFIGURATION);
        }
        return new PackageJson(jsonObj);
    }

    private static boolean fileExists(URI url, TruffleLanguage.Env env) {
        return CommonJSResolution.fileExists(env.getPublicTruffleFile(url));
    }

    private static boolean isFileURI(URI maybe) {
        return maybe != null && maybe.getScheme().equals(FILE);
    }

    private static URI getPackageUrl(String packageSpecifier, TruffleFile parentURL) {
        try {
            URI combined = new URI("./" + NODE_MODULES + "/" + packageSpecifier);
            TruffleFile resolved = parentURL.resolve(String.valueOf(combined));
            return resolved.toUri();
        } catch (URISyntaxException e) {
            // will handle null return
        }
        return null;
    }

    private static URI getParentUrl(URI scopeUrl, TruffleLanguage.Env env) {
        TruffleFile asFile = env.getPublicTruffleFile(scopeUrl);
        if (asFile.getParent() != null) {
            return asFile.getParent().toUri();
        }
        return null;
    }

    private static Format getAssociatedDefaultFormat(URI resolved) {
        assert resolved.getPath() != null;
        if (resolved.getPath().endsWith(MJS_EXT)) {
            return Format.ESM;
        }
        // By default, try loading as CJS if not .mjs
        return Format.CommonJS;
    }

    private static boolean isDirectory(URI resolved, TruffleLanguage.Env env) {
        return env.getPublicTruffleFile(resolved).isDirectory();
    }

    private static URI resolveRelativeToParent(String specifier, URI parentURL) {
        return parentURL.resolve(specifier);
    }

    private TruffleFile getFullPath(ScriptOrModule referencingModule) {
        String refPath = referencingModule == null ? null : referencingModule.getSource().getPath();
        if (refPath == null) {
            refPath = realm.getContextOptions().getRequireCwd();
        }
        return realm.getEnv().getPublicTruffleFile(refPath);
    }

    @TruffleBoundary
    private static JSException failMessage(String message) {
        return JSException.create(JSErrorType.TypeError, message);
    }

    @TruffleBoundary
    private static JSException fail(String errorType, String moduleIdentifier) {
        return failMessage(errorType + moduleIdentifier + Strings.SINGLE_QUOTE);
    }

    private static boolean isRelativePathFileName(String moduleIdentifier) {
        return moduleIdentifier.startsWith(DOT_SLASH) || moduleIdentifier.startsWith(DOT_DOT_SLASH);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy