2026-05-12 10:32:42 -07:00
|
|
|
/* Copyright (C) 2026 Wildfire Games.
|
2025-01-15 11:38:37 -08:00
|
|
|
* This file is part of 0 A.D.
|
|
|
|
|
*
|
|
|
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "precompiled.h"
|
|
|
|
|
|
|
|
|
|
#include "ModuleLoader.h"
|
|
|
|
|
|
2025-08-04 10:04:30 -07:00
|
|
|
#include "js/Modules.h"
|
|
|
|
|
#include "lib/file/file_system.h"
|
|
|
|
|
#include "lib/file/vfs/vfs.h"
|
|
|
|
|
#include "lib/os_path.h"
|
|
|
|
|
#include "lib/status.h"
|
|
|
|
|
#include "ps/CLogger.h"
|
2025-01-15 11:38:37 -08:00
|
|
|
#include "ps/CStr.h"
|
2025-08-04 10:04:30 -07:00
|
|
|
#include "ps/Errors.h"
|
2025-01-15 11:38:37 -08:00
|
|
|
#include "ps/Filesystem.h"
|
2025-01-15 12:11:53 -08:00
|
|
|
#include "scriptinterface/FunctionWrapper.h"
|
2025-01-15 11:38:37 -08:00
|
|
|
#include "scriptinterface/Object.h"
|
|
|
|
|
#include "scriptinterface/ScriptConversions.h"
|
2025-08-04 10:04:30 -07:00
|
|
|
#include "scriptinterface/ScriptExceptions.h"
|
2026-05-12 10:32:42 -07:00
|
|
|
#include "scriptinterface/Interface.h"
|
|
|
|
|
#include "scriptinterface/Request.h"
|
2025-01-15 11:38:37 -08:00
|
|
|
|
2025-08-04 10:04:30 -07:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <filesystem>
|
2025-01-15 11:38:37 -08:00
|
|
|
#include <fmt/format.h>
|
2025-08-04 10:04:30 -07:00
|
|
|
#include <js/CallArgs.h>
|
|
|
|
|
#include <js/Class.h>
|
|
|
|
|
#include <js/CompileOptions.h>
|
|
|
|
|
#include <js/Object.h>
|
|
|
|
|
#include <js/Promise.h>
|
|
|
|
|
#include <js/SourceText.h>
|
|
|
|
|
#include <js/Value.h>
|
|
|
|
|
#include <jsapi.h>
|
2025-01-24 23:48:52 -08:00
|
|
|
#include <numeric>
|
2025-01-15 11:38:37 -08:00
|
|
|
#include <stdexcept>
|
2025-08-04 10:04:30 -07:00
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
|
|
|
|
|
class JSObject;
|
|
|
|
|
namespace mozilla { union Utf8Unit; }
|
|
|
|
|
struct JSContext;
|
2025-01-15 11:38:37 -08:00
|
|
|
|
|
|
|
|
namespace Script
|
|
|
|
|
{
|
|
|
|
|
namespace
|
|
|
|
|
{
|
2025-01-24 23:48:52 -08:00
|
|
|
/**
|
|
|
|
|
* When provided with an appendix name (containing a "~" and ending with
|
|
|
|
|
* ".append.js") the name of the base file is returned. When it's not an
|
|
|
|
|
* appendix name an empty string is returned. E.g.
|
|
|
|
|
* "base_file~mod_name.append.js" -> "base_file.js"
|
|
|
|
|
* "base-name~0.append.js" -> "base-name.js"
|
|
|
|
|
* "base_file~mod_name.js" -> ""
|
|
|
|
|
* "base_file_mod_name.append.js" -> ""
|
|
|
|
|
*/
|
|
|
|
|
VfsPath GetBaseFilename(const VfsPath& filename)
|
|
|
|
|
{
|
|
|
|
|
constexpr std::string_view appendixExtension{".append.js"};
|
|
|
|
|
const std::string nameString{filename.string8()};
|
|
|
|
|
if (nameString.size() < appendixExtension.size())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
if (nameString.substr(nameString.size() - appendixExtension.size()) != appendixExtension)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
const size_t pos{nameString.find('~')};
|
|
|
|
|
if (pos == std::string::npos)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
return nameString.substr(0, pos) + ".js";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] std::vector<VfsPath> GetAppendices(const VfsPath& baseFilepath)
|
|
|
|
|
{
|
|
|
|
|
const VfsPath directory{baseFilepath.Parent()};
|
|
|
|
|
CFileInfos fileInfos;
|
|
|
|
|
if (g_VFS->GetDirectoryEntries(baseFilepath, &fileInfos, nullptr) != INFO::OK)
|
|
|
|
|
{
|
|
|
|
|
throw std::runtime_error{fmt::format("Unable to load files in directory: \"{}\"",
|
|
|
|
|
directory.string8())};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<VfsPath> filenames;
|
|
|
|
|
std::transform(fileInfos.begin(), fileInfos.end(), std::back_inserter(filenames),
|
|
|
|
|
[](const CFileInfo fileInfo)
|
|
|
|
|
{
|
|
|
|
|
return fileInfo.Name();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const VfsPath baseFilename{baseFilepath.Filename()};
|
|
|
|
|
const auto endPoint = std::remove_if(filenames.begin(), filenames.end(), [&](const VfsPath& filename)
|
|
|
|
|
{
|
|
|
|
|
const VfsPath base{GetBaseFilename(filename)};
|
|
|
|
|
return base != baseFilename;
|
|
|
|
|
});
|
|
|
|
|
filenames.erase(endPoint, filenames.end());
|
|
|
|
|
|
|
|
|
|
for (VfsPath& filename : filenames)
|
|
|
|
|
filename = directory / filename;
|
|
|
|
|
return filenames;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-05 06:15:53 -08:00
|
|
|
[[nodiscard]] std::string GetCode(const ModuleLoader::AllowModuleFunc& allowModule,
|
|
|
|
|
const VfsPath& filePath)
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
2025-02-05 06:15:53 -08:00
|
|
|
if (!allowModule || !allowModule(filePath))
|
|
|
|
|
{
|
|
|
|
|
throw std::runtime_error{fmt::format("Importing file \"{}\" is disallowed.",
|
|
|
|
|
filePath.string8())};
|
|
|
|
|
}
|
2025-01-15 11:38:37 -08:00
|
|
|
if (!VfsFileExists(filePath))
|
|
|
|
|
throw std::runtime_error{fmt::format("The file \"{}\" does not exist.", filePath.string8())};
|
|
|
|
|
|
2025-10-04 16:11:03 -07:00
|
|
|
if (filePath.Extension() != L".js")
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
|
|
|
|
throw std::runtime_error{fmt::format("The file \"{}\" is not a JavaScript module.",
|
|
|
|
|
filePath.string8())};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CVFSFile file;
|
|
|
|
|
const PSRETURN ret{file.Load(g_VFS, filePath)};
|
|
|
|
|
if (ret != PSRETURN_OK)
|
|
|
|
|
{
|
|
|
|
|
throw std::runtime_error{fmt::format("Failed to load file \"{}\": {}.", filePath.string8(),
|
|
|
|
|
GetErrorString(ret))};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return file.DecodeUTF8();
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
template<typename Requester>
|
2026-05-12 10:32:42 -07:00
|
|
|
[[nodiscard]] JSObject* CompileModule(const Script::Request& rq,
|
2025-02-05 06:15:53 -08:00
|
|
|
const ModuleLoader::AllowModuleFunc& allowModule, ModuleLoader::RegistryType& registry,
|
2025-01-29 10:28:48 -08:00
|
|
|
const VfsPath& filePath, Requester&& requester)
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
2025-04-03 10:59:05 -07:00
|
|
|
const VfsPath normalizedPath{filePath.fileSystemPath().lexically_normal().generic_string()};
|
2025-02-05 06:15:53 -08:00
|
|
|
const auto insertResult = registry.try_emplace(normalizedPath, rq, allowModule, normalizedPath);
|
2025-01-29 10:28:48 -08:00
|
|
|
ModuleLoader::CompiledModule& compiledModule{std::get<1>(*std::get<0>(insertResult))};
|
|
|
|
|
compiledModule.AddRequester(std::forward<Requester>(requester));
|
|
|
|
|
return compiledModule.m_ModuleObject;
|
2025-01-15 11:38:37 -08:00
|
|
|
}
|
2026-05-12 10:32:42 -07:00
|
|
|
[[nodiscard]] JSObject* Resolve(const Script::Request& rq, const ModuleLoader::AllowModuleFunc& allowModule,
|
2025-02-05 06:15:53 -08:00
|
|
|
ModuleLoader::RegistryType& registry, JS::HandleValue referencingModule,
|
|
|
|
|
JS::HandleObject moduleRequest)
|
2025-01-15 13:08:06 -08:00
|
|
|
{
|
|
|
|
|
std::string includeString;
|
|
|
|
|
const JS::RootedValue pathValue{rq.cx,
|
|
|
|
|
JS::StringValue(JS::GetModuleRequestSpecifier(rq.cx, moduleRequest))};
|
|
|
|
|
if (!Script::FromJSVal(rq, pathValue, includeString))
|
|
|
|
|
throw std::logic_error{"The module-name to import isn't a string."};
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
std::string includingModule;
|
|
|
|
|
if (!Script::FromJSProperty(rq, referencingModule, "path", includingModule))
|
|
|
|
|
throw std::logic_error{"The importing module doesn't have a \"path\" property."};
|
|
|
|
|
|
2025-02-05 06:15:53 -08:00
|
|
|
return CompileModule(rq, allowModule, registry, includeString, includingModule);
|
2025-01-15 13:08:06 -08:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 10:32:42 -07:00
|
|
|
[[nodiscard]] JSObject* Evaluate(const Script::Request& rq, JS::HandleObject mod)
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
|
|
|
|
if (!JS::ModuleLink(rq.cx, mod))
|
|
|
|
|
{
|
|
|
|
|
ScriptException::CatchPending(rq);
|
|
|
|
|
throw std::invalid_argument{"Unable to link module."};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JS::RootedValue val{rq.cx};
|
2025-01-15 12:01:23 -08:00
|
|
|
if (!JS::ModuleEvaluate(rq.cx, mod, &val) || !val.isObject())
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
|
|
|
|
ScriptException::CatchPending(rq);
|
|
|
|
|
throw std::invalid_argument{"Unable to evaluate module."};
|
|
|
|
|
}
|
2025-01-15 12:01:23 -08:00
|
|
|
|
|
|
|
|
return &val.toObject();
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
Status FileChangedHook(void* param, const VfsPath& changedFile)
|
|
|
|
|
{
|
|
|
|
|
ModuleLoader::RegistryType& registry{*static_cast<ModuleLoader::RegistryType*>(param)};
|
|
|
|
|
|
|
|
|
|
const VfsPath proposedBasePath{GetBaseFilename(changedFile)};
|
|
|
|
|
|
|
|
|
|
std::vector<VfsPath> modulesToErase{proposedBasePath.empty() ? changedFile : proposedBasePath};
|
|
|
|
|
std::vector<std::reference_wrapper<ModuleLoader::Result>> queries;
|
|
|
|
|
while (!modulesToErase.empty())
|
|
|
|
|
{
|
|
|
|
|
const VfsPath path{modulesToErase.back()};
|
|
|
|
|
modulesToErase.pop_back();
|
|
|
|
|
const VfsPath pathWithExtension{path.ChangeExtension(".js")};
|
|
|
|
|
const auto it = registry.find(pathWithExtension);
|
|
|
|
|
if (it == registry.end())
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
ModuleLoader::CompiledModule compiledModule{std::move(std::get<1>(*it))};
|
|
|
|
|
registry.erase(it);
|
|
|
|
|
|
|
|
|
|
const auto [additionalModules, callbacks] = compiledModule.GetRequesters();
|
|
|
|
|
modulesToErase.insert(modulesToErase.end(),
|
|
|
|
|
additionalModules.begin(), additionalModules.end());
|
|
|
|
|
|
|
|
|
|
queries.insert(queries.end(), callbacks.begin(), callbacks.end());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (ModuleLoader::Result& result : queries)
|
|
|
|
|
result.Resume();
|
|
|
|
|
|
|
|
|
|
return INFO::OK;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-15 12:11:53 -08:00
|
|
|
template<bool reject>
|
2025-01-15 12:01:23 -08:00
|
|
|
bool Call(JSContext* cx, const unsigned argc, JS::Value* vp)
|
|
|
|
|
{
|
|
|
|
|
JS::CallArgs args{JS::CallArgsFromVp(argc, vp)};
|
2026-05-12 10:32:42 -07:00
|
|
|
const Script::Request rq{cx};
|
2025-01-15 12:01:23 -08:00
|
|
|
|
|
|
|
|
const auto statusPtr{JS::GetMaybePtrFromReservedSlot<ModuleLoader::Future::Status>(
|
|
|
|
|
&args.callee(), 0)};
|
|
|
|
|
if (!statusPtr)
|
|
|
|
|
return true;
|
|
|
|
|
|
2025-01-15 12:11:53 -08:00
|
|
|
auto& status = *statusPtr;
|
|
|
|
|
|
|
|
|
|
if (reject)
|
|
|
|
|
{
|
|
|
|
|
JS::HandleValue error{args.get(0)};
|
|
|
|
|
std::string asString;
|
|
|
|
|
ScriptFunction::Call(rq, error, "toString", asString);
|
|
|
|
|
std::string stack;
|
|
|
|
|
Script::GetProperty(rq, error, "stack", stack);
|
|
|
|
|
status = ModuleLoader::Future::Rejected{std::make_exception_ptr(std::runtime_error{
|
|
|
|
|
asString + '\n' + stack})};
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-15 12:34:58 -08:00
|
|
|
const auto evaluatingStatus{std::get_if<ModuleLoader::Future::Evaluating>(&status)};
|
|
|
|
|
if (!evaluatingStatus)
|
|
|
|
|
{
|
|
|
|
|
status = ModuleLoader::Future::Rejected{std::make_exception_ptr(std::runtime_error{
|
|
|
|
|
"Future is not Pending."})};
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
status = ModuleLoader::Future::Fulfilled{evaluatingStatus->moduleNamespace};
|
2025-01-15 12:01:23 -08:00
|
|
|
return true;
|
2025-01-15 11:38:37 -08:00
|
|
|
}
|
2025-01-15 12:01:23 -08:00
|
|
|
|
2025-01-15 12:11:53 -08:00
|
|
|
template<bool reject>
|
2025-01-15 12:01:23 -08:00
|
|
|
constexpr JSClassOps callbackClassOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
2025-01-15 12:11:53 -08:00
|
|
|
/*call =*/Call<reject>, nullptr, nullptr};
|
2025-01-15 12:01:23 -08:00
|
|
|
|
2025-01-15 12:11:53 -08:00
|
|
|
template<bool reject>
|
|
|
|
|
constexpr JSClass callbackClass{"Callback", JSCLASS_HAS_RESERVED_SLOTS(1), &callbackClassOps<reject>};
|
2025-01-15 11:38:37 -08:00
|
|
|
} // anonymous namespace
|
|
|
|
|
|
2026-05-12 10:32:42 -07:00
|
|
|
ModuleLoader::CompiledModule::CompiledModule(const Script::Request& rq, const AllowModuleFunc& allowModule,
|
2025-02-05 06:15:53 -08:00
|
|
|
const VfsPath& filePath):
|
2025-01-15 11:38:37 -08:00
|
|
|
m_ModuleObject(rq.cx)
|
|
|
|
|
{
|
2025-01-24 23:48:52 -08:00
|
|
|
const std::vector<VfsPath> appendices{GetAppendices(filePath)};
|
2025-02-05 06:15:53 -08:00
|
|
|
const std::string code{std::accumulate(appendices.begin(), appendices.end(),
|
|
|
|
|
GetCode(allowModule, filePath),
|
|
|
|
|
[&](std::string code, const VfsPath& fileToAppend)
|
2025-01-24 23:48:52 -08:00
|
|
|
{
|
2025-02-05 06:15:53 -08:00
|
|
|
return std::move(code) + GetCode(allowModule, fileToAppend);
|
2025-01-24 23:48:52 -08:00
|
|
|
})};
|
2025-01-15 11:38:37 -08:00
|
|
|
|
|
|
|
|
JS::CompileOptions options{rq.cx};
|
|
|
|
|
const std::string filePathStr{filePath.string8()};
|
|
|
|
|
options.setFileAndLine(filePathStr.c_str(), 1);
|
|
|
|
|
|
|
|
|
|
JS::SourceText<mozilla::Utf8Unit> src;
|
|
|
|
|
if (!src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed))
|
|
|
|
|
throw std::invalid_argument{fmt::format("Unable to read code file: \"{}\".", filePathStr)};
|
|
|
|
|
|
|
|
|
|
m_ModuleObject = JS::CompileModule(rq.cx, options, src);
|
|
|
|
|
|
|
|
|
|
if (!m_ModuleObject)
|
|
|
|
|
{
|
|
|
|
|
ScriptException::CatchPending(rq);
|
|
|
|
|
throw std::invalid_argument{fmt::format("Unable to compile module: \"{}\".",
|
|
|
|
|
filePathStr)};
|
|
|
|
|
}
|
2025-01-15 13:15:08 -08:00
|
|
|
|
|
|
|
|
JS::RootedValue modInfo{rq.cx};
|
|
|
|
|
Script::CreateObject(rq, &modInfo, "path", filePathStr);
|
|
|
|
|
JS::SetModulePrivate(m_ModuleObject, modInfo);
|
2025-01-15 11:38:37 -08:00
|
|
|
}
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
[[nodiscard]] std::tuple<const std::vector<VfsPath>&,
|
|
|
|
|
const std::vector<std::reference_wrapper<ModuleLoader::Result>>&>
|
|
|
|
|
ModuleLoader::CompiledModule::GetRequesters() const
|
|
|
|
|
{
|
|
|
|
|
return {m_Importer, m_Callbacks};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleLoader::CompiledModule::AddRequester(VfsPath importer)
|
|
|
|
|
{
|
|
|
|
|
m_Importer.push_back(std::move(importer));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleLoader::CompiledModule::AddRequester(Result& callback)
|
|
|
|
|
{
|
|
|
|
|
m_Callbacks.push_back(callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleLoader::CompiledModule::RemoveRequester(Result* toErase)
|
|
|
|
|
{
|
|
|
|
|
m_Callbacks.erase(std::remove_if(m_Callbacks.begin(), m_Callbacks.end(),
|
|
|
|
|
[&](Result& elem)
|
|
|
|
|
{
|
|
|
|
|
return &elem == toErase;
|
|
|
|
|
}), m_Callbacks.end());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 10:32:42 -07:00
|
|
|
ModuleLoader::Future::Future(const Script::Request& rq, ModuleLoader& loader, Result& result,
|
2025-01-29 10:28:48 -08:00
|
|
|
VfsPath modulePath):
|
2025-01-15 12:34:58 -08:00
|
|
|
m_Status{Evaluating{{rq.cx, nullptr}, {rq.cx, JS_NewObject(rq.cx, &callbackClass<false>)},
|
2025-01-15 12:11:53 -08:00
|
|
|
{rq.cx, JS_NewObject(rq.cx, &callbackClass<true>)}}}
|
2025-01-15 12:01:23 -08:00
|
|
|
{
|
2025-01-15 12:34:58 -08:00
|
|
|
// It's possible to access exported values before the complete module is evaluated (whenever
|
|
|
|
|
// something is `export`-ed before a top-level `await`).
|
|
|
|
|
// Those "partial" module namespaces are not exposed for the following reasons:
|
|
|
|
|
// - The use case for them is too limited.
|
|
|
|
|
// - JS developers are used to getting either a complete namespace or nothing.
|
|
|
|
|
// - Accessing values which are not yet exported results in an error. These errors might implicitly be
|
|
|
|
|
// dropped.
|
|
|
|
|
|
2025-02-05 06:15:53 -08:00
|
|
|
JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_AllowModule, loader.m_Registry, modulePath,
|
|
|
|
|
result)};
|
2025-01-15 12:01:23 -08:00
|
|
|
JS::RootedObject promise{rq.cx, Evaluate(rq, mod)};
|
2025-01-15 12:34:58 -08:00
|
|
|
Evaluating& evaluatingStatus{std::get<Evaluating>(m_Status)};
|
|
|
|
|
evaluatingStatus.moduleNamespace = JS::GetModuleNamespace(rq.cx, mod);
|
2025-01-15 12:01:23 -08:00
|
|
|
|
|
|
|
|
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
|
|
|
|
|
|
2025-01-15 12:11:53 -08:00
|
|
|
if (!JS::AddPromiseReactions(rq.cx, promise, evaluatingStatus.fulfill, evaluatingStatus.reject))
|
2025-01-15 12:01:23 -08:00
|
|
|
throw std::runtime_error{"Failed adding promise reaction."};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::Future::Future(Future&& other) noexcept:
|
|
|
|
|
m_Status{std::exchange(other.m_Status, Invalid{})}
|
|
|
|
|
{
|
|
|
|
|
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::Future& ModuleLoader::Future::operator=(Future&& other) noexcept
|
|
|
|
|
{
|
|
|
|
|
SetReservedSlot(JS::UndefinedValue());
|
|
|
|
|
m_Status = std::exchange(other.m_Status, Invalid{});
|
|
|
|
|
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
|
|
|
|
|
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::Future::~Future()
|
|
|
|
|
{
|
|
|
|
|
SetReservedSlot(JS::UndefinedValue());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] bool ModuleLoader::Future::IsDone() const noexcept
|
|
|
|
|
{
|
2025-01-15 12:11:53 -08:00
|
|
|
return std::holds_alternative<Fulfilled>(m_Status) || std::holds_alternative<Rejected>(m_Status);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-15 12:34:58 -08:00
|
|
|
[[nodiscard]] JSObject* ModuleLoader::Future::Get()
|
2025-01-15 12:11:53 -08:00
|
|
|
{
|
|
|
|
|
if (std::holds_alternative<Fulfilled>(m_Status))
|
2025-01-15 12:34:58 -08:00
|
|
|
return std::get<Fulfilled>(std::exchange(m_Status, Invalid{})).moduleNamespace;
|
2025-01-15 12:11:53 -08:00
|
|
|
std::exception_ptr error{std::move(std::get<Rejected>(m_Status).error)};
|
|
|
|
|
m_Status = Invalid{};
|
|
|
|
|
std::rethrow_exception(std::move(error));
|
2025-01-15 12:01:23 -08:00
|
|
|
}
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
[[nodiscard]] bool ModuleLoader::Future::IsWaiting() const noexcept
|
|
|
|
|
{
|
|
|
|
|
return std::holds_alternative<WaitingForFileChange>(m_Status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleLoader::Future::SetWaiting() noexcept
|
|
|
|
|
{
|
|
|
|
|
m_Status.emplace<WaitingForFileChange>();
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-15 12:01:23 -08:00
|
|
|
void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
|
|
|
|
|
{
|
|
|
|
|
Evaluating* evaluatingStatus{std::get_if<Evaluating>(&m_Status)};
|
|
|
|
|
if (!evaluatingStatus)
|
|
|
|
|
return;
|
|
|
|
|
if (evaluatingStatus->fulfill)
|
|
|
|
|
JS::SetReservedSlot(evaluatingStatus->fulfill, 0, privateValue);
|
2025-01-15 12:11:53 -08:00
|
|
|
if (evaluatingStatus->reject)
|
|
|
|
|
JS::SetReservedSlot(evaluatingStatus->reject, 0, privateValue);
|
2025-01-15 12:01:23 -08:00
|
|
|
}
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
ModuleLoader::Result::iterator::iterator(Result& backReference):
|
|
|
|
|
backRef{&backReference}
|
|
|
|
|
{}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] ModuleLoader::Future& ModuleLoader::Result::iterator::operator*() const
|
|
|
|
|
{
|
|
|
|
|
return backRef->m_Storage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] ModuleLoader::Future* ModuleLoader::Result::iterator::operator->() const
|
|
|
|
|
{
|
|
|
|
|
return &(**this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::Result::iterator& ModuleLoader::Result::iterator::operator++()
|
|
|
|
|
{
|
|
|
|
|
backRef->m_Storage.SetWaiting();
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::Result::iterator& ModuleLoader::Result::iterator::operator++(int)
|
|
|
|
|
{
|
|
|
|
|
++(*this);
|
|
|
|
|
// All iterator of this `LoadModuleResult` refere to the same `LoadModuleResult`.
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:45:54 -07:00
|
|
|
[[nodiscard]] bool ModuleLoader::Result::iterator::operator==(const iterator&) const
|
2025-01-29 10:28:48 -08:00
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:45:54 -07:00
|
|
|
[[nodiscard]] bool ModuleLoader::Result::iterator::operator!=(const iterator&) const
|
2025-01-29 10:28:48 -08:00
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 10:32:42 -07:00
|
|
|
ModuleLoader::Result::Result(const Script::Request& rq, const VfsPath& modulePath):
|
2025-06-11 05:51:52 -07:00
|
|
|
m_Script{rq.GetScriptInterface()},
|
2025-01-29 10:28:48 -08:00
|
|
|
m_ModulePath{modulePath},
|
2025-06-11 05:51:52 -07:00
|
|
|
m_Storage{rq, m_Script.GetModuleLoader(), *this, m_ModulePath}
|
2025-01-29 10:28:48 -08:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::Result::~Result()
|
|
|
|
|
{
|
2025-06-11 05:51:52 -07:00
|
|
|
ModuleLoader::RegistryType& registry{m_Script.GetModuleLoader().m_Registry};
|
|
|
|
|
const auto modIter = registry.find(m_ModulePath);
|
|
|
|
|
if (modIter == registry.end())
|
2025-01-29 10:28:48 -08:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
std::get<1>(*modIter).RemoveRequester(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] ModuleLoader::Result::iterator ModuleLoader::Result::begin() noexcept
|
|
|
|
|
{
|
|
|
|
|
return ModuleLoader::Result::iterator{*this};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] ModuleLoader::Result::iterator ModuleLoader::Result::end() const noexcept
|
|
|
|
|
{
|
|
|
|
|
return ModuleLoader::Result::iterator{};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModuleLoader::Result::Resume()
|
|
|
|
|
{
|
|
|
|
|
if (m_Storage.IsWaiting())
|
2025-06-11 05:51:52 -07:00
|
|
|
m_Storage = ModuleLoader::Future{m_Script, m_Script.GetModuleLoader(), *this, m_ModulePath};
|
2025-01-29 10:28:48 -08:00
|
|
|
}
|
|
|
|
|
|
2025-02-05 06:15:53 -08:00
|
|
|
ModuleLoader::ModuleLoader(ModuleLoader::AllowModuleFunc allowModule):
|
|
|
|
|
m_AllowModule{std::move(allowModule)}
|
2025-01-29 10:28:48 -08:00
|
|
|
{
|
|
|
|
|
RegisterFileReloadFunc(FileChangedHook, static_cast<void*>(&m_Registry));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleLoader::~ModuleLoader()
|
|
|
|
|
{
|
|
|
|
|
UnregisterFileReloadFunc(FileChangedHook, static_cast<void*>(&m_Registry));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 10:32:42 -07:00
|
|
|
[[nodiscard]] ModuleLoader::Result ModuleLoader::LoadModule(const Script::Request& rq,
|
2025-01-15 12:01:23 -08:00
|
|
|
const VfsPath& modulePath)
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
2025-01-29 10:28:48 -08:00
|
|
|
return Result{rq, modulePath};
|
2025-01-15 11:38:37 -08:00
|
|
|
}
|
|
|
|
|
|
2025-01-15 13:15:08 -08:00
|
|
|
/**
|
|
|
|
|
* This is only executed once per module. Following accesses of `import.meta`
|
|
|
|
|
* evaluate to the same object.
|
|
|
|
|
*/
|
|
|
|
|
[[nodiscard]] bool ModuleLoader::MetadataHook(JSContext* cx, JS::HandleValue privateValue,
|
|
|
|
|
JS::HandleObject metaObject) noexcept
|
|
|
|
|
{
|
2026-05-12 10:32:42 -07:00
|
|
|
const Script::Request rq{cx};
|
2025-01-15 13:15:08 -08:00
|
|
|
|
|
|
|
|
JS::RootedValue path{cx};
|
|
|
|
|
if (!Script::GetProperty(rq, privateValue, "path", &path))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
JS::RootedValue metaValue{cx, JS::ObjectValue(*metaObject)};
|
|
|
|
|
if (!Script::SetProperty(rq, metaValue, "path", path))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 10:28:48 -08:00
|
|
|
[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue referencingPrivate,
|
|
|
|
|
JS::HandleObject request) noexcept
|
2025-01-15 11:38:37 -08:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-12 10:32:42 -07:00
|
|
|
const Script::Request rq{cx};
|
2025-02-05 06:15:53 -08:00
|
|
|
ModuleLoader& loader{rq.GetScriptInterface().GetModuleLoader()};
|
|
|
|
|
return Resolve(rq, loader.m_AllowModule, loader.m_Registry, referencingPrivate, request);
|
2025-01-15 11:38:37 -08:00
|
|
|
}
|
|
|
|
|
catch (const std::exception& e)
|
|
|
|
|
{
|
|
|
|
|
LOGERROR("%s", e.what());
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
catch (...)
|
|
|
|
|
{
|
|
|
|
|
LOGERROR("Error compiling module.");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-15 13:08:06 -08:00
|
|
|
|
|
|
|
|
[[nodiscard]] bool ModuleLoader::DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate,
|
|
|
|
|
JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept
|
|
|
|
|
{
|
2026-05-12 10:32:42 -07:00
|
|
|
const Script::Request rq{cx};
|
2025-01-15 13:08:06 -08:00
|
|
|
try
|
|
|
|
|
{
|
2025-02-05 06:15:53 -08:00
|
|
|
ModuleLoader& loader{rq.GetScriptInterface().GetModuleLoader()};
|
|
|
|
|
JS::RootedObject mod{rq.cx, Resolve(rq, loader.m_AllowModule, loader.m_Registry,
|
2025-01-29 10:28:48 -08:00
|
|
|
referencingPrivate, moduleRequest)};
|
2025-01-15 13:08:06 -08:00
|
|
|
JS::RootedObject evaluationPromise{rq.cx, Evaluate(rq, mod)};
|
|
|
|
|
return JS::FinishDynamicModuleImport(rq.cx, evaluationPromise, referencingPrivate,
|
|
|
|
|
moduleRequest, promise);
|
|
|
|
|
}
|
|
|
|
|
catch (const std::exception& e)
|
|
|
|
|
{
|
|
|
|
|
LOGERROR("%s", e.what());
|
|
|
|
|
return JS::FinishDynamicModuleImport(rq.cx, nullptr, referencingPrivate, moduleRequest,
|
|
|
|
|
promise);
|
|
|
|
|
}
|
|
|
|
|
catch (...)
|
|
|
|
|
{
|
|
|
|
|
return JS::FinishDynamicModuleImport(rq.cx, nullptr, referencingPrivate, moduleRequest,
|
|
|
|
|
promise);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-15 11:38:37 -08:00
|
|
|
} // namespace Script
|