mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-09-21 20:06:40 +00:00
Allow modules to be hotloaded
Reload a module when it's script file got changed or when one of it's imported modules got reloaded. This doesn't work for dynamic `import`s.
This commit is contained in:
@@ -114,16 +114,19 @@ VfsPath GetBaseFilename(const VfsPath& filename)
|
||||
return file.DecodeUTF8();
|
||||
}
|
||||
|
||||
template<typename Requester>
|
||||
[[nodiscard]] JSObject* CompileModule(const ScriptRequest& rq, ModuleLoader::RegistryType& registry,
|
||||
const VfsPath& filePath)
|
||||
const VfsPath& filePath, Requester&& requester)
|
||||
{
|
||||
const VfsPath normalizedPath{filePath.fileSystemPath().lexically_normal().generic_string()};
|
||||
const auto insertResult = registry.try_emplace(normalizedPath, rq, normalizedPath);
|
||||
return std::get<1>(*std::get<0>(insertResult)).m_ModuleObject;
|
||||
ModuleLoader::CompiledModule& compiledModule{std::get<1>(*std::get<0>(insertResult))};
|
||||
compiledModule.AddRequester(std::forward<Requester>(requester));
|
||||
return compiledModule.m_ModuleObject;
|
||||
}
|
||||
|
||||
[[nodiscard]] JSObject* Resolve(const ScriptRequest& rq,
|
||||
ModuleLoader::RegistryType& registry, JS::HandleObject moduleRequest)
|
||||
[[nodiscard]] JSObject* Resolve(const ScriptRequest& rq, ModuleLoader::RegistryType& registry,
|
||||
JS::HandleValue referencingModule, JS::HandleObject moduleRequest)
|
||||
{
|
||||
std::string includeString;
|
||||
const JS::RootedValue pathValue{rq.cx,
|
||||
@@ -131,7 +134,11 @@ VfsPath GetBaseFilename(const VfsPath& filename)
|
||||
if (!Script::FromJSVal(rq, pathValue, includeString))
|
||||
throw std::logic_error{"The module-name to import isn't a string."};
|
||||
|
||||
return CompileModule(rq, registry, includeString);
|
||||
std::string includingModule;
|
||||
if (!Script::FromJSProperty(rq, referencingModule, "path", includingModule))
|
||||
throw std::logic_error{"The importing module doesn't have a \"path\" property."};
|
||||
|
||||
return CompileModule(rq, registry, includeString, includingModule);
|
||||
}
|
||||
|
||||
[[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
|
||||
@@ -152,6 +159,39 @@ VfsPath GetBaseFilename(const VfsPath& filename)
|
||||
return &val.toObject();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
template<bool reject>
|
||||
bool Call(JSContext* cx, const unsigned argc, JS::Value* vp)
|
||||
{
|
||||
@@ -228,7 +268,34 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP
|
||||
JS::SetModulePrivate(m_ModuleObject, modInfo);
|
||||
}
|
||||
|
||||
ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath):
|
||||
[[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());
|
||||
}
|
||||
|
||||
ModuleLoader::Future::Future(const ScriptRequest& rq, RegistryType& registry, Result& result,
|
||||
VfsPath modulePath):
|
||||
m_Status{Evaluating{{rq.cx, nullptr}, {rq.cx, JS_NewObject(rq.cx, &callbackClass<false>)},
|
||||
{rq.cx, JS_NewObject(rq.cx, &callbackClass<true>)}}}
|
||||
{
|
||||
@@ -240,7 +307,7 @@ ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, cons
|
||||
// - Accessing values which are not yet exported results in an error. These errors might implicitly be
|
||||
// dropped.
|
||||
|
||||
JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)};
|
||||
JS::RootedObject mod{rq.cx, CompileModule(rq, registry, modulePath, result)};
|
||||
JS::RootedObject promise{rq.cx, Evaluate(rq, mod)};
|
||||
Evaluating& evaluatingStatus{std::get<Evaluating>(m_Status)};
|
||||
evaluatingStatus.moduleNamespace = JS::GetModuleNamespace(rq.cx, mod);
|
||||
@@ -285,6 +352,16 @@ ModuleLoader::Future::~Future()
|
||||
std::rethrow_exception(std::move(error));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ModuleLoader::Future::IsWaiting() const noexcept
|
||||
{
|
||||
return std::holds_alternative<WaitingForFileChange>(m_Status);
|
||||
}
|
||||
|
||||
void ModuleLoader::Future::SetWaiting() noexcept
|
||||
{
|
||||
m_Status.emplace<WaitingForFileChange>();
|
||||
}
|
||||
|
||||
void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
|
||||
{
|
||||
Evaluating* evaluatingStatus{std::get_if<Evaluating>(&m_Status)};
|
||||
@@ -296,10 +373,90 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
|
||||
JS::SetReservedSlot(evaluatingStatus->reject, 0, privateValue);
|
||||
}
|
||||
|
||||
[[nodiscard]] ModuleLoader::Future ModuleLoader::LoadModule(const ScriptRequest& rq,
|
||||
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;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ModuleLoader::Result::iterator::operator==(const iterator&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ModuleLoader::Result::iterator::operator!=(const iterator&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ModuleLoader::Result::Result(const ScriptRequest& rq, const VfsPath& modulePath):
|
||||
m_Cx{rq.cx},
|
||||
m_Registry{rq.GetScriptInterface().GetModuleLoader().m_Registry},
|
||||
m_ModulePath{modulePath},
|
||||
m_Storage{rq, m_Registry, *this, m_ModulePath}
|
||||
{
|
||||
}
|
||||
|
||||
ModuleLoader::Result::~Result()
|
||||
{
|
||||
const auto modIter = m_Registry.find(m_ModulePath);
|
||||
if (modIter == m_Registry.end())
|
||||
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())
|
||||
m_Storage = ModuleLoader::Future{m_Cx, m_Registry, *this, m_ModulePath};
|
||||
}
|
||||
|
||||
ModuleLoader::ModuleLoader()
|
||||
{
|
||||
RegisterFileReloadFunc(FileChangedHook, static_cast<void*>(&m_Registry));
|
||||
}
|
||||
|
||||
ModuleLoader::~ModuleLoader()
|
||||
{
|
||||
UnregisterFileReloadFunc(FileChangedHook, static_cast<void*>(&m_Registry));
|
||||
}
|
||||
|
||||
[[nodiscard]] ModuleLoader::Result ModuleLoader::LoadModule(const ScriptRequest& rq,
|
||||
const VfsPath& modulePath)
|
||||
{
|
||||
return Future{rq, *this, modulePath};
|
||||
return Result{rq, modulePath};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,13 +479,14 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue,
|
||||
JS::HandleObject moduleRequest) noexcept
|
||||
[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue referencingPrivate,
|
||||
JS::HandleObject request) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
const ScriptRequest rq{cx};
|
||||
return Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, moduleRequest);
|
||||
return Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, referencingPrivate,
|
||||
request);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@@ -349,7 +507,7 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
|
||||
try
|
||||
{
|
||||
JS::RootedObject mod{rq.cx, Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry,
|
||||
moduleRequest)};
|
||||
referencingPrivate, moduleRequest)};
|
||||
JS::RootedObject evaluationPromise{rq.cx, Evaluate(rq, mod)};
|
||||
return JS::FinishDynamicModuleImport(rq.cx, evaluationPromise, referencingPrivate,
|
||||
moduleRequest, promise);
|
||||
|
||||
@@ -21,9 +21,12 @@
|
||||
#include "lib/file/vfs/vfs_path.h"
|
||||
#include "scriptinterface/ScriptTypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
class ScriptContext;
|
||||
class ScriptRequest;
|
||||
@@ -35,80 +38,162 @@ class ModuleLoader
|
||||
public:
|
||||
friend ScriptContext;
|
||||
|
||||
class CompiledModule
|
||||
{
|
||||
public:
|
||||
CompiledModule(const ScriptRequest& rq, const VfsPath& filePath);
|
||||
JS::PersistentRootedObject m_ModuleObject;
|
||||
};
|
||||
class CompiledModule;
|
||||
class Future;
|
||||
class Result;
|
||||
|
||||
using RegistryType = std::unordered_map<VfsPath, CompiledModule>;
|
||||
|
||||
class Future
|
||||
{
|
||||
public:
|
||||
struct Evaluating
|
||||
{
|
||||
JS::PersistentRootedObject moduleNamespace;
|
||||
JS::PersistentRootedObject fulfill;
|
||||
JS::PersistentRootedObject reject;
|
||||
};
|
||||
struct Fulfilled
|
||||
{
|
||||
JS::PersistentRootedObject moduleNamespace;
|
||||
};
|
||||
struct Rejected
|
||||
{
|
||||
std::exception_ptr error;
|
||||
};
|
||||
struct Invalid {};
|
||||
using Status = std::variant<Evaluating, Fulfilled, Rejected, Invalid>;
|
||||
|
||||
explicit Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath);
|
||||
Future() = default;
|
||||
Future(const Future&) = delete;
|
||||
Future& operator=(const Future&) = delete;
|
||||
Future(Future&& other) noexcept;
|
||||
Future& operator=(Future&& other) noexcept;
|
||||
~Future();
|
||||
|
||||
[[nodiscard]] bool IsDone() const noexcept;
|
||||
|
||||
/**
|
||||
* Throws if the evaluation of the module failed.
|
||||
* @return The module namespace. All exported values are a property
|
||||
* of this object. @c default is a property with name "default".
|
||||
*/
|
||||
[[nodiscard]] JSObject* Get();
|
||||
|
||||
private:
|
||||
// It's save to not require a `JS::HandleValue` here.
|
||||
void SetReservedSlot(JS::Value privateValue) noexcept;
|
||||
|
||||
Status m_Status{Invalid{}};
|
||||
};
|
||||
ModuleLoader();
|
||||
ModuleLoader(const ModuleLoader&) = delete;
|
||||
ModuleLoader& operator=(const ModuleLoader&) = delete;
|
||||
ModuleLoader(ModuleLoader&&) = delete;
|
||||
ModuleLoader& operator=(ModuleLoader&&) = delete;
|
||||
~ModuleLoader();
|
||||
|
||||
/**
|
||||
* Load the specified module and all module it imports recursively.
|
||||
*
|
||||
* @param rq @c globalThis is taken from this @c ScriptRequest.
|
||||
* @param modulePath The path to the file which should be loaded as a module.
|
||||
* @return A future that is fulfilled when the evaluation of the module
|
||||
* completes.
|
||||
* @param modulePath The path to the file which should be loaded as a
|
||||
* module.
|
||||
* @return A range of futures. The compilation of the first future is
|
||||
* already started. The evaluation of the subsequent futures start once
|
||||
* the module file is edited.
|
||||
*/
|
||||
[[nodiscard]] Future LoadModule(const ScriptRequest& rq, const VfsPath& modulePath);
|
||||
[[nodiscard]] Result LoadModule(const ScriptRequest& rq, const VfsPath& modulePath);
|
||||
|
||||
private:
|
||||
// Functions used by the `ScriptContext`.
|
||||
[[nodiscard]] static bool MetadataHook(JSContext* cx, JS::HandleValue privateValue,
|
||||
JS::HandleObject metaObject) noexcept;
|
||||
[[nodiscard]] static JSObject* ResolveHook(JSContext* cx, JS::HandleValue,
|
||||
[[nodiscard]] static JSObject* ResolveHook(JSContext* cx, JS::HandleValue referencingPrivate,
|
||||
JS::HandleObject moduleRequest) noexcept;
|
||||
[[nodiscard]] static bool DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate,
|
||||
JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept;
|
||||
|
||||
RegistryType m_Registry;
|
||||
};
|
||||
|
||||
class ModuleLoader::CompiledModule
|
||||
{
|
||||
public:
|
||||
CompiledModule(const ScriptRequest& rq, const VfsPath& filePath);
|
||||
|
||||
std::tuple<const std::vector<VfsPath>&,
|
||||
const std::vector<std::reference_wrapper<Result>>&> GetRequesters() const;
|
||||
|
||||
void AddRequester(VfsPath importer);
|
||||
void AddRequester(Result& callback);
|
||||
void RemoveRequester(Result* toErase);
|
||||
|
||||
JS::PersistentRootedObject m_ModuleObject;
|
||||
private:
|
||||
std::vector<VfsPath> m_Importer;
|
||||
std::vector<std::reference_wrapper<Result>> m_Callbacks;
|
||||
};
|
||||
|
||||
/**
|
||||
* The future is fulfilled once the evaluation of the module
|
||||
* completes.
|
||||
* Note: The evaluation might not be started yet.
|
||||
*/
|
||||
class ModuleLoader::Future
|
||||
{
|
||||
friend Result;
|
||||
public:
|
||||
struct Evaluating
|
||||
{
|
||||
JS::PersistentRootedObject moduleNamespace;
|
||||
JS::PersistentRootedObject fulfill;
|
||||
JS::PersistentRootedObject reject;
|
||||
};
|
||||
struct Fulfilled
|
||||
{
|
||||
JS::PersistentRootedObject moduleNamespace;
|
||||
};
|
||||
struct Rejected
|
||||
{
|
||||
std::exception_ptr error;
|
||||
};
|
||||
struct WaitingForFileChange {};
|
||||
struct Invalid {};
|
||||
using Status = std::variant<Evaluating, Fulfilled, Rejected, WaitingForFileChange, Invalid>;
|
||||
|
||||
explicit Future(const ScriptRequest& rq, RegistryType& reqistry, Result& result, VfsPath modulePath);
|
||||
Future() = default;
|
||||
Future(const Future&) = delete;
|
||||
Future& operator=(const Future&) = delete;
|
||||
Future(Future&& other) noexcept;
|
||||
Future& operator=(Future&& other) noexcept;
|
||||
~Future();
|
||||
|
||||
[[nodiscard]] bool IsDone() const noexcept;
|
||||
|
||||
/**
|
||||
* Throws if the evaluation of the module failed.
|
||||
* @return The module namespace. All exported values are a property
|
||||
* of this object. @c default is a property with name "default".
|
||||
*/
|
||||
[[nodiscard]] JSObject* Get();
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool IsWaiting() const noexcept;
|
||||
void SetWaiting() noexcept;
|
||||
|
||||
// It's save to not require a `JS::HandleValue` here.
|
||||
void SetReservedSlot(JS::Value privateValue) noexcept;
|
||||
|
||||
Status m_Status{Invalid{}};
|
||||
};
|
||||
|
||||
class ModuleLoader::Result
|
||||
{
|
||||
class iterator;
|
||||
public:
|
||||
explicit Result(const ScriptRequest& rq, const VfsPath& modulePath);
|
||||
Result(const Result&) = delete;
|
||||
Result& operator=(const Result&) = delete;
|
||||
Result(Result&&) = delete;
|
||||
Result& operator=(Result&&) = delete;
|
||||
~Result();
|
||||
|
||||
[[nodiscard]] iterator begin() noexcept;
|
||||
[[nodiscard]] iterator end() const noexcept;
|
||||
|
||||
void Resume();
|
||||
|
||||
private:
|
||||
JSContext* m_Cx;
|
||||
RegistryType& m_Registry;
|
||||
VfsPath m_ModulePath;
|
||||
Future m_Storage;
|
||||
};
|
||||
|
||||
class ModuleLoader::Result::iterator
|
||||
{
|
||||
public:
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = Future;
|
||||
using pointer = value_type*;
|
||||
using reference = value_type&;
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
|
||||
explicit iterator() = default;
|
||||
explicit iterator(Result& backReference);
|
||||
|
||||
[[nodiscard]] reference operator*() const;
|
||||
[[nodiscard]] pointer operator->() const;
|
||||
|
||||
iterator& operator++();
|
||||
iterator& operator++(int);
|
||||
|
||||
[[nodiscard]] bool operator==(const iterator&);
|
||||
[[nodiscard]] bool operator!=(const iterator&);
|
||||
|
||||
private:
|
||||
Result* backRef{nullptr};
|
||||
};
|
||||
} // namespace Script
|
||||
|
||||
#endif // INCLUDED_SCRIPTMODULELOADER
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
#include "lib/self_test.h"
|
||||
|
||||
#include "lib/file/vfs/vfs.h"
|
||||
#include "lib/sysdep/dir_watch.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/Filesystem.h"
|
||||
#include "scriptinterface/FunctionWrapper.h"
|
||||
@@ -26,11 +28,53 @@
|
||||
#include "scriptinterface/ScriptContext.h"
|
||||
#include "scriptinterface/ScriptInterface.h"
|
||||
|
||||
#if OS_LINUX
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
#if OS_WIN || OS_WIN64 || OS_MAC || OS_MACOSX
|
||||
#include <filesystem>
|
||||
#endif
|
||||
#include <fstream>
|
||||
|
||||
Status wdir_watch_Init();
|
||||
Status wdir_watch_Shutdown();
|
||||
|
||||
namespace
|
||||
{
|
||||
void ClearFromCache(const VfsPath& path)
|
||||
{
|
||||
#if OS_BSD
|
||||
TS_SKIP("On BSD hotload isn't implemented.");
|
||||
#endif
|
||||
|
||||
OsPath file;
|
||||
if (g_VFS->GetRealPath(path, file) != INFO::OK)
|
||||
throw std::exception{};
|
||||
PDirWatch dirWatch;
|
||||
dir_watch_Add((file.Parent() / "").string8(), dirWatch);
|
||||
|
||||
#if OS_WIN || OS_WIN64 || OS_MAC || OS_MACOSX
|
||||
std::filesystem::last_write_time(file.string8(), std::filesystem::file_time_type::clock::now());
|
||||
#endif
|
||||
|
||||
#if OS_LINUX
|
||||
// On Linux only this aproach seems to trigger a file reload.
|
||||
if (std::system(("touch " + file.string8()).c_str()) != 0)
|
||||
throw std::runtime_error{"`touch` didn't work."};
|
||||
#endif
|
||||
|
||||
ReloadChangedFiles();
|
||||
}
|
||||
}
|
||||
|
||||
class TestScriptModule : public CxxTest::TestSuite
|
||||
{
|
||||
public:
|
||||
void setUp()
|
||||
{
|
||||
if constexpr (OS_WIN)
|
||||
wdir_watch_Init();
|
||||
|
||||
g_VFS = CreateVfs();
|
||||
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.scriptinterface" / "module" / "",
|
||||
VFS_MOUNT_MUST_EXIST));
|
||||
@@ -39,6 +83,9 @@ public:
|
||||
void tearDown()
|
||||
{
|
||||
g_VFS.reset();
|
||||
|
||||
if constexpr (OS_WIN)
|
||||
wdir_watch_Shutdown();
|
||||
}
|
||||
|
||||
void test_StaticImport()
|
||||
@@ -123,8 +170,9 @@ public:
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
|
||||
|
||||
auto& future = *result.begin();
|
||||
TS_ASSERT(!future.IsDone());
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future.IsDone());
|
||||
@@ -136,8 +184,9 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js");
|
||||
|
||||
auto& future = *result.begin();
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(!future.IsDone());
|
||||
TS_ASSERT_THROWS_ANYTHING(std::ignore = future.Get());
|
||||
@@ -148,8 +197,8 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
Script::ModuleLoader::Future future0{
|
||||
script.GetModuleLoader().LoadModule(rq, "empty.js")};
|
||||
auto result{script.GetModuleLoader().LoadModule(rq, "empty.js")};
|
||||
Script::ModuleLoader::Future& future0{*result.begin()};
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future0.IsDone());
|
||||
@@ -168,8 +217,9 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
Script::ModuleLoader::Future future0{
|
||||
script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")};
|
||||
auto result{script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")};
|
||||
Script::ModuleLoader::Future& future0{*result.begin()};
|
||||
|
||||
Script::ModuleLoader::Future future1{std::move(future0)};
|
||||
Script::ModuleLoader::Future future2;
|
||||
future2 = std::move(future1);
|
||||
@@ -189,11 +239,13 @@ public:
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
auto future{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")};
|
||||
auto blabbermouthResult{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")};
|
||||
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
auto future = std::move(*blabbermouthResult.begin());
|
||||
TS_ASSERT(!future.IsDone());
|
||||
|
||||
future = script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
auto emptyResult{script.GetModuleLoader().LoadModule(rq, "empty.js")};
|
||||
future = std::move(*emptyResult.begin());
|
||||
TS_ASSERT(!future.IsDone());
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
@@ -208,8 +260,9 @@ public:
|
||||
|
||||
// To silence the error.
|
||||
const TestLogger _;
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_throw.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "top_level_throw.js");
|
||||
|
||||
auto& future = *result.begin();
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future.IsDone());
|
||||
TS_ASSERT_THROWS_EQUALS(std::ignore = future.Get(), const std::runtime_error& e, e.what(),
|
||||
@@ -221,9 +274,10 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
@@ -248,17 +302,17 @@ public:
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
{
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedObject ns{rq.cx, result.begin()->Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12));
|
||||
}
|
||||
|
||||
{
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "include/../export.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "include/../export.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedObject ns{rq.cx, result.begin()->Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
int value{0};
|
||||
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
|
||||
@@ -272,17 +326,17 @@ public:
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
{
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedObject ns{rq.cx, result.begin()->Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12));
|
||||
}
|
||||
|
||||
{
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "indirect.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "indirect.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedObject ns{rq.cx, result.begin()->Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
int value{0};
|
||||
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
|
||||
@@ -295,10 +349,10 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "export_default/immutable.js");
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export_default/immutable.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
@@ -324,10 +378,11 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "export_default/does_not_work_around.js");
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export_default/does_not_work_around.js");
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
@@ -342,10 +397,10 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "export_default/works_around.js");
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export_default/works_around.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
@@ -359,8 +414,10 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
|
||||
future = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
auto awaitResult = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
|
||||
auto future = std::move(*awaitResult.begin());
|
||||
auto exportResult = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
future = std::move(*exportResult.begin());
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
@@ -376,10 +433,10 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "dynamic_import.js");
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "dynamic_import.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
@@ -407,10 +464,10 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "meta.js");
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "meta.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
const JS::RootedValue modNamespace{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
@@ -427,15 +484,186 @@ public:
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "modified/base.js");
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "modified/base.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
|
||||
auto& future = *result.begin();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
std::string result;
|
||||
TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "fn", result));
|
||||
TS_ASSERT_STR_EQUALS(result, "Base01");
|
||||
std::string returnValue;
|
||||
TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "fn", returnValue));
|
||||
TS_ASSERT_STR_EQUALS(returnValue, "Base01");
|
||||
}
|
||||
|
||||
void test_Hotload()
|
||||
{
|
||||
constexpr int goal{2};
|
||||
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
int counter{0};
|
||||
for (auto& future : script.GetModuleLoader().LoadModule(rq, "empty.js"))
|
||||
{
|
||||
TS_ASSERT(!future.IsDone());
|
||||
|
||||
if (counter != 0)
|
||||
ClearFromCache("empty.js");
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future.IsDone());
|
||||
|
||||
if (counter == goal)
|
||||
break;
|
||||
|
||||
++counter;
|
||||
}
|
||||
|
||||
TS_ASSERT_EQUALS(counter, goal);
|
||||
}
|
||||
|
||||
void test_HotloadWithoutIncrement()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(result.begin()->IsDone());
|
||||
ClearFromCache("top_level_await_finite.js");
|
||||
TS_ASSERT(result.begin()->IsDone());
|
||||
}
|
||||
|
||||
void test_HotloadIndipendence()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
// It's intended to be used as in the test above but it's easier to test when it's unrolled.
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
|
||||
auto iter = result.begin();
|
||||
{
|
||||
auto& future = *iter;
|
||||
g_ScriptContext->RunJobs();
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
const JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12));
|
||||
|
||||
int value{0};
|
||||
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
|
||||
TS_ASSERT_EQUALS(value, 12);
|
||||
}
|
||||
++iter;
|
||||
{
|
||||
auto& future = *iter;
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(!future.IsDone());
|
||||
ClearFromCache("export.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future.IsDone());
|
||||
|
||||
JS::RootedObject ns{rq.cx, future.Get()};
|
||||
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
|
||||
|
||||
int value{0};
|
||||
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
|
||||
TS_ASSERT_DIFFERS(value, 12);
|
||||
TS_ASSERT_EQUALS(value, 6);
|
||||
}
|
||||
}
|
||||
|
||||
void test_HotloadModified()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
auto iter = result.begin();
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(iter->IsDone());
|
||||
|
||||
++iter;
|
||||
TS_ASSERT(!iter->IsDone());
|
||||
|
||||
ClearFromCache("empty~trigger.append.js");
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(iter->IsDone());
|
||||
}
|
||||
|
||||
void test_HotloadIndirect()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "indirect.js");
|
||||
auto iter = result.begin();
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(iter->IsDone());
|
||||
|
||||
++iter;
|
||||
ClearFromCache("export.js");
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(iter->IsDone());
|
||||
}
|
||||
|
||||
void test_HotloadUnobserved()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
{
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
|
||||
{
|
||||
TestLogger logger;
|
||||
ClearFromCache("blabbermouth.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
|
||||
{
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
}
|
||||
|
||||
void test_HotloadAfterResultDestruction()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
{
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
auto result = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
|
||||
auto iter = result.begin();
|
||||
TS_ASSERT(iter->IsDone());
|
||||
++iter;
|
||||
}
|
||||
|
||||
TestLogger logger;
|
||||
ClearFromCache("blabbermouth.js");
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
|
||||
void test_ResultDestructionAfterScriptRequestDestruction()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
auto _ = script.GetModuleLoader().LoadModule(ScriptRequest{script}, "empty.js");
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user