Index: source/scriptinterface/ScriptExtraHeaders.h =================================================================== --- source/scriptinterface/ScriptExtraHeaders.h +++ source/scriptinterface/ScriptExtraHeaders.h @@ -59,6 +59,7 @@ #include "js/SourceText.h" #include "js/Proxy.h" #include "js/Warnings.h" +#include #undef signbit Index: source/scriptinterface/ScriptInterface.h =================================================================== --- source/scriptinterface/ScriptInterface.h +++ source/scriptinterface/ScriptInterface.h @@ -203,6 +203,11 @@ */ bool LoadGlobalScriptFile(const VfsPath& path) const; + /** + * Load an ES module. VFS must be initialized. + */ + bool LoadESModule(const VfsPath& path) const; + /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -23,6 +23,7 @@ #include "ScriptInterface.h" #include "ScriptStats.h" #include "StructuredClone.h" +#include "ScriptModuleLoader.h" #include "lib/debug.h" #include "lib/utf8.h" @@ -684,6 +685,12 @@ return LoadGlobalScript(path, code); } +bool ScriptInterface::LoadESModule(const VfsPath& path) const +{ + ScriptRequest rq(this); + return ScriptModuleLoader::Evaluate(rq.cx, path); +} + bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); Index: source/scriptinterface/ScriptModuleLoader.h =================================================================== --- /dev/null +++ source/scriptinterface/ScriptModuleLoader.h @@ -0,0 +1,48 @@ +/* Copyright (C) 2022 Wildfire Games. + * 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 . + */ + +#ifndef INCLUDED_SCRIPTMODULELOADER +#define INCLUDED_SCRIPTMODULELOADER + +#include "lib/file/vfs/vfs_path.h" +#include "ps/Errors.h" +#include "scriptinterface/ScriptConversions.h" +#include "scriptinterface/ScriptExceptions.h" +#include "scriptinterface/ScriptRequest.h" +#include "scriptinterface/ScriptTypes.h" + +#include +#include + +namespace ScriptModuleLoader +{ + typedef std::map ModuleCache; + + bool Evaluate(JSContext* cx, const VfsPath& modulePath); + + JSObject* ModuleResolutionHook(JSContext* cx, JS::HandleValue referencingModule, JS::HandleString path); + + JSObject* CompileModule(JSContext* cx, const VfsPath& filePath); + + VfsPath GetScriptPath(JSContext* cx, JS::HandleValue moduleHandle); + + VfsPath NormalizeScriptPath(const VfsPath& path); + + static std::map ResolvedModules; +}; + +#endif // INCLUDED_SCRIPTMODULELOADER Index: source/scriptinterface/ScriptModuleLoader.cpp =================================================================== --- /dev/null +++ source/scriptinterface/ScriptModuleLoader.cpp @@ -0,0 +1,178 @@ +/* Copyright (C) 2022 Wildfire Games. + * 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 . + */ + +#include "precompiled.h" + +#include "ScriptModuleLoader.h" + +#include "ScriptContext.h" +#include "ScriptConversions.h" +#include "ScriptExtraHeaders.h" + +#include "lib/debug.h" +#include "lib/utf8.h" +#include "ps/CLogger.h" +#include "ps/Filesystem.h" +#include "ps/CStr.h" + +#include +#include +#include + +VfsPath ScriptModuleLoader::NormalizeScriptPath(const VfsPath& path) +{ + VfsPath ret; + + std::vector dirs; + std::wstringstream wss(path.string()); + std::wstring dir; + + while (std::getline(wss, dir, L'/')) + { + dirs.push_back(dir); + } + + for (const std::wstring& dir : dirs) + { + if (dir == L".") { + continue; + } + + if (dir == L"..") { + ret = ret.Parent(); + continue; + } + + ret = ret / VfsPath(dir); + } + + return ret; +} + +JSObject* ScriptModuleLoader::CompileModule(JSContext* cx, const VfsPath& filePath) +{ + ScriptRequest rq(cx); + if (!VfsFileExists(filePath)) + { + LOGERROR("File '%s' does not exist", filePath.string8()); + return nullptr; + } + + if (filePath.Extension() != L".mjs" && false) + { + LOGERROR("File '%s' is not an ES module", filePath.string8()); + return nullptr; + } + + CVFSFile file; + + PSRETURN ret = file.Load(g_VFS, filePath); + if (ret != PSRETURN_OK) + { + LOGERROR("Failed to load file '%s': %s", filePath.string8(), GetErrorString(ret)); + return nullptr; + } + + CStr code = file.DecodeUTF8(); + + JS::CompileOptions options(cx); + options.setFileAndLine(filePath.string8().c_str(), 1); + + JS::SourceText src; + ENSURE(src.init(cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); + + JS::RootedObject mod(cx); + mod = JS::CompileModule(cx, options, src); + + JS::RootedObject modInfo(cx, JS_NewPlainObject(cx)); + JS::RootedValue modPath(cx); + Script::ToJSVal(rq, &modPath, filePath.Parent()); + JS_DefineProperty(cx, modInfo, "path", modPath, JSPROP_ENUMERATE); + JS::SetModulePrivate(mod, JS::ObjectValue(*modInfo)); + + return mod; +} + +VfsPath ScriptModuleLoader::GetScriptPath(JSContext* cx, JS::HandleValue moduleHandle) +{ + std::wstring ret; + + if (!moduleHandle.isUndefined()) + { + ScriptRequest rq(cx); + Script::FromJSProperty(rq, moduleHandle, "path", ret); + } + + return VfsPath(ret); +} + +JSObject* ScriptModuleLoader::ModuleResolutionHook(JSContext* cx, JS::HandleValue referencingModule, JS::HandleString path) +{ + ScriptRequest rq(cx); + + size_t bufferLen; + JS::AutoCheckCannotGC nogc; + const unsigned char* strBuffer = JS_GetLatin1StringCharsAndLength(cx, nogc, path, &bufferLen); + std::wstring pathString; + pathString.assign(strBuffer, strBuffer + bufferLen); + + VfsPath filename = VfsPath(pathString); + VfsPath refFilename = GetScriptPath(cx, referencingModule); + + VfsPath actualPath = NormalizeScriptPath(refFilename / filename); + + ModuleCache::iterator it = ResolvedModules.find(actualPath); + if (it != ResolvedModules.end()) + { + return it->second; + } + + JS::RootedObject mod(cx, CompileModule(cx, actualPath)); + + if (!mod) + { + ScriptException::CatchPending(rq); + return nullptr; + } + + ResolvedModules.emplace(actualPath, JS::PersistentRootedObject(cx, mod)); + + return mod; +} + +bool ScriptModuleLoader::Evaluate(JSContext* cx, const VfsPath& modulePath) +{ + ScriptRequest rq(cx); + + JS::RootedObject global(cx, rq.glob); + if (!global) + { + return false; + } + + JS::SetModuleResolveHook(JS_GetRuntime(cx), ModuleResolutionHook); + + JS::RootedObject topModule(cx, CompileModule(cx, modulePath)); + + if (!topModule || !JS::ModuleInstantiate(cx, topModule) || !JS::ModuleEvaluate(cx, topModule)) + { + ScriptException::CatchPending(rq); + return false; + } + + return true; +}