Skip to content

File nodeModuleLoaderFeature.h

File List > features > nodeModuleLoaderFeature.h

Go to the documentation of this file

#pragma once

#include <jac/machine/machine.h>
#include <jac/machine/values.h>

#include <iostream>
#include <cassert>
#include <sstream>
#include <optional>
#include <vector>
#include <algorithm>
#include <vector>
#include <string>


// XXX: experimental and not fully implemented and tested
//
// To avoid using URLs (i.e., "file://" in our case), we identify paths by initial "./".
// Because quickjs performs automatic of relative imports to current module URL, we can
// only differentiate relative imports ("./", "../") and package imports by initial characters
// of "module_name". We therefore expect the entry point to have a file path starting with "./".


namespace jac {


bool strContains(const auto& str, const auto& substr) {
    return str.find(substr) != std::string::npos;
}

std::string_view substring(const std::string& str, size_t pos, size_t count = std::string::npos) {
    return std::string_view(str).substr(pos, count);
}


template<class Next>
class NodeModuleLoaderFeature : public Next {
private:
    // XXX: parentURL is always empty and thus ignored

    static std::optional<std::string> PACKAGE_TARGET_RESOLVE(const std::string& packageURL, jac::Value target, std::optional<std::string> patternMatch, NodeModuleLoaderFeature<Next>& self) {
        // XXX: isImports is always false for now
        // XXX: conditions is always default for now

        if (target.isString()) {
            auto targetStr = target.toString();
            if (!targetStr.starts_with("./")) {
                throw jac::Exception::create(jac::Exception::Type::Error, "invalid package target");
            }
            auto prevSlash = targetStr.find_first_of("/\\");
            decltype(prevSlash) nextSlash;
            while ((nextSlash = targetStr.find_first_of("/\\", prevSlash + 1)) != std::string::npos) {
                auto segment = targetStr.substr(prevSlash + 1, nextSlash - prevSlash - 1);
                if (segment == "" || segment == "." || segment == ".." || segment == "node_modules") {
                    throw jac::Exception::create(jac::Exception::Type::Error, "invalid package target");
                }
                prevSlash = nextSlash;
            }
            std::string resolvedTarget = self.path.join({ packageURL, targetStr });  // XXX: "URL resolution"
            assert(strContains(resolvedTarget, packageURL));

            if (!patternMatch) {
                return resolvedTarget;
            }

            prevSlash = patternMatch->find_first_of("/\\");
            while ((nextSlash = patternMatch->find_first_of("/\\", prevSlash + 1)) != std::string::npos) {
                auto segment = substring(*patternMatch, prevSlash + 1, nextSlash - prevSlash - 1);
                if (segment == "" || segment == "." || segment == ".." || segment == "node_modules") {
                    throw jac::Exception::create(jac::Exception::Type::Error, "invalid module specifier");
                }
                prevSlash = nextSlash;
            }

            std::stringstream ss;
            for (char c : resolvedTarget) {
                if (c == '*') {
                    ss << *patternMatch;
                }
                else {
                    ss << c;
                }
            }
            return ss.str();  // TODO: "URL resolution"
        }
        else if (target.isObject()) {
            throw jac::Exception::create(jac::Exception::Type::Error, "object targets not implemented");
        }
        else if (target.isArray()) {
            throw jac::Exception::create(jac::Exception::Type::Error, "array targets not implemented");
        }
        else if (target.isNull()) {
            return std::nullopt;
        }
        else {
            throw jac::Exception::create(jac::Exception::Type::Error, "invalid package target");
        }
    }

    static std::optional<std::string> PACKAGE_IMPORTS_EXPORTS_RESOLVE(const std::string& matchKey, jac::Object matchObj, const std::string& packageURL, NodeModuleLoaderFeature<Next>& self) {
        // XXX: isImports is always false for now
        // XXX: conditions is always default for now

        if (matchKey.ends_with("/")) {
            throw jac::Exception::create(jac::Exception::Type::Error, "invalid module specifier");
        }
        if (!strContains(matchKey, "*") && matchObj.hasProperty(matchKey)) {
            auto target = matchObj.get<jac::Value>(matchKey);
            return PACKAGE_TARGET_RESOLVE(packageURL, target, std::nullopt, self);
        }
        std::vector<jac::StringView> expansionKeys;
        {
            auto keys = matchObj.getOwnPropertyNames();
            for (auto& key : keys) {
                auto keyStr = key.toString();
                auto starPos = keyStr.find('*');
                if (starPos == std::string::npos) {
                    continue;
                }
                if (keyStr.find('*', starPos + 1) != std::string::npos) {
                    continue;
                }
                expansionKeys.emplace_back(std::move(keyStr));
            }
        }

        // FIXME: sort expansionKeys by specificity ("PATTERN_KEY_COMPARE" in docs)

        for (auto& expansionKey : expansionKeys) {
            size_t starPos = expansionKey.find('*');
            auto patternBase = expansionKey.substr(0, starPos);
            if (!matchKey.starts_with(patternBase)) {
                continue;
            }
            auto patternTrailer = expansionKey.substr(starPos + 1);
            if (patternTrailer.empty() || (matchKey.size() >= expansionKey.size() && matchKey.ends_with(patternTrailer))) {
                auto target = matchObj.get<jac::Value>(expansionKey);
                auto patternMatch = matchKey.substr(patternBase.size(), matchKey.size() - patternBase.size() - patternTrailer.size());
                return PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, self);
            }
        }

        return std::nullopt;
    }

    static std::string PACKAGE_EXPORTS_RESOLVE(const std::string& packageURL, const std::string& subpath, jac::Value exports, NodeModuleLoaderFeature<Next>& self) {
        // XXX: non-default conditions not supported
        bool objOnlyDots = false;
        if (exports.isObject()) {
            auto obj = exports.to<jac::Object>();
            auto props = obj.getOwnPropertyNames();
            bool anyDot = false;
            bool anyNonDot = false;
            for (auto& prop : props) {
                if (prop.toString().starts_with(".")) {
                    anyDot = true;
                }
                else {
                    anyNonDot = true;
                }
            }
            if (anyDot && anyNonDot) {
                throw jac::Exception::create(jac::Exception::Type::Error, "invalid package configuration");
            }
            objOnlyDots = anyDot;
        }

        std::optional<std::string> resolved = std::nullopt;
        if (subpath == ".") {
            jac::Value mainExport = jac::Value::undefined(self.context());
            if (exports.isString() || exports.isArray()) {
                mainExport = exports;
            }
            else if (exports.isObject()) {
                if (!objOnlyDots) {
                    mainExport = exports;
                }
                else {
                    auto obj = exports.to<jac::Object>();
                    if (obj.hasProperty(".")) {
                        mainExport = obj.get<jac::Value>(".");
                    }
                }
            }
            if (!mainExport.isUndefined()) {
                resolved = PACKAGE_TARGET_RESOLVE(packageURL, mainExport, std::nullopt, self);
            }
        }
        else if (exports.isObject() && objOnlyDots) {
            assert(subpath.starts_with("./"));
            resolved = PACKAGE_IMPORTS_EXPORTS_RESOLVE(subpath, exports, packageURL, self);
        }

        if (!resolved) {
            throw jac::Exception::create(jac::Exception::Type::Error, "package path not exported");
        }

        return *resolved;
    }

    static std::optional<std::string> PACKAGE_SELF_RESOLVE(const std::string& packageName, const std::string& packageSubpath, NodeModuleLoaderFeature<Next>& self) {
        jac::Object pjson = READ_PACKAGE_JSON(".", self);
        if (pjson.hasProperty("name")) {
            auto nameVal = pjson.get<jac::Value>("name");
            if (nameVal.isString() && nameVal.toString() == packageName) {
                if (pjson.hasProperty("exports")) {
                    auto exports = pjson.get<jac::Value>("exports");
                    if (!exports.isUndefined() && !exports.isNull()) {
                        return PACKAGE_EXPORTS_RESOLVE(".", packageSubpath, exports, self);
                    }
                }
            }
        }
        return std::nullopt;
    }

    static jac::Object READ_PACKAGE_JSON(const std::string& packageURL, NodeModuleLoaderFeature<Next>& self) {
        std::string pjsonURL = packageURL + "/package.json";

        std::string pjsonStr = self.fs.loadCode(pjsonURL);
        return jac::Object(self.context(), JS_ParseJSON2(self.context(), pjsonStr.c_str(), pjsonStr.size(), pjsonURL.c_str(), 0));
    }

    static std::string PACKAGE_RESOLVE(const std::string& packageSpecifier, NodeModuleLoaderFeature<Next>& self) {
        if (packageSpecifier.size() == 0) {
            throw jac::Exception::create(jac::Exception::Type::Error, "invalid module specifier");
        }

        std::string packageName;
        std::string packageSubpath;
        size_t after = 0;

        if (packageSpecifier.starts_with("@")) {
            auto slashPos = packageSpecifier.find('/');
            if (slashPos == std::string::npos) {
                throw jac::Exception::create(jac::Exception::Type::Error, "invalid module specifier");
            }
            after = slashPos + 1;
        }

        auto slashPos = packageSpecifier.find('/', after);
        if (slashPos == std::string::npos) {
            packageName = packageSpecifier;
            packageSubpath = ".";
        }
        else {
            packageName = packageSpecifier.substr(0, slashPos);
            packageSubpath = "." + packageSpecifier.substr(slashPos);
        }

        if (packageName.starts_with('.') || std::any_of(packageName.begin(), packageName.end(), [](char c){ return c == '\\' || c == '%'; })) {
            throw jac::Exception::create(jac::Exception::Type::Error, "invalid module specifier");
        }

        if (auto selfUrl = PACKAGE_SELF_RESOLVE(packageName, packageSubpath, self)) {
            return *selfUrl;
        }

        // XXX: we do not try parent directories for node_modules - difficult because of qjs, also not needed for now

        std::string packageURL = "node_modules/" + packageName;  // XXX: resolve URL relative to parentURL
        if (!self.fs.isDirectoryCode(packageURL)) {
            throw jac::Exception::create(jac::Exception::Type::Error, "module not found");
        }
        jac::Object pjson = READ_PACKAGE_JSON(packageURL, self);

        if (pjson.hasProperty("exports")) {
            auto exports = pjson.get<jac::Value>("exports");
            if (!exports.isUndefined() && !exports.isNull()) {
                return PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, exports, self);
            }
        }
        if (packageSubpath == ".") {
            if (pjson.hasProperty("main")) {
                auto mainVal = pjson.get<jac::Value>("main");
                if (mainVal.isString()) {
                    std::string mainFile = mainVal.toString();
                    return packageURL + "/" + mainFile;
                }
            }
        }
        else {
            return packageURL + "/" + packageSubpath;
        }

        throw jac::Exception::create(jac::Exception::Type::Error, "module not found");
    }

    static std::string ESM_RESOLVE(const std::string& specifier, NodeModuleLoaderFeature<Next>& self) {
        // XXX: parentURL is always empty -> ignored
        // XXX: url specifiers not supported
        // XXX: format is always set to "module"

        std::string resolved;
        if (specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with("/")) {
            resolved = specifier;
        }
        else if (specifier.starts_with("#")) {
            throw jac::Exception::create(jac::Exception::Type::Error, "not implemented (#module specifiers)");
        }
        else {
            resolved = PACKAGE_RESOLVE(specifier, self);
        }

        size_t start = 0;
        size_t percentPos;
        while ((percentPos = resolved.find('%', start)) != std::string::npos) {
            if (percentPos + 2 >= resolved.size()) {
                break;
            }
            std::string_view hexStr = std::string_view(resolved).substr(percentPos + 1, 2);
            if (hexStr == "2F" || hexStr == "2f" || hexStr == "5C" || hexStr == "5c") {
                throw jac::Exception::create(jac::Exception::Type::Error, "invalid module specifier");
            }
            start = percentPos + 1;
        }

        if (self.fs.isDirectoryCode(resolved)) {
            throw jac::Exception::create(jac::Exception::Type::Error, "unsupported directory import");
        }
        if (!self.fs.existsCode(resolved) || !self.fs.isFileCode(resolved)) {
            throw jac::Exception::create(jac::Exception::Type::Error, "module not found");
        }

        return resolved;
    }

    static JSModuleDef *moduleLoaderCbk(JSContext* ctx, const char *module_name, void *_self) {
        auto &self = *static_cast<NodeModuleLoaderFeature<Next>*>(_self);

        std::string filename = ESM_RESOLVE(module_name, self);

        std::string buffer;
        try {
            buffer = self.fs.loadCode(filename);
        } catch (jac::Exception &e) {
            e.throwJS(ctx);
            return nullptr;
        }

        // compile and return module
        self.resetWatchdog();
        JSValue val = JS_Eval(ctx, buffer.c_str(), buffer.size(), module_name,
                              JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
        if (JS_IsException(val)) {
            return nullptr;
        }

        auto mdl = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(val));

        Object meta(ctx, JS_GetImportMeta(ctx, mdl));
        meta.set("url", filename);
        meta.set("main", false);

        return mdl;
    }

public:
    Value evalFile(std::string path_) {
        auto buffer = this->fs.loadCode(path_);

        Value val = this->eval(std::move(buffer), path_, EvalFlags::Module);
        return val;
    }

    void evalFileWithEventLoop(std::string path_) {
        Value promise = this->evalFile(path_);
        this->evalWithEventLoopCommon(promise);
    }

    void initialize() {
        Next::initialize();

        JS_SetModuleLoaderFunc(this->runtime(), nullptr, moduleLoaderCbk, this);
    }
};


} // namespace jac