| // Copyright 2026 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| #include "src/dawn/node/standalone/Polyfills.h" | |
| #include <algorithm> | |
| #include <cctype> | |
| #include <chrono> | |
| #include <cstdint> | |
| #include <filesystem> | |
| #include <fstream> | |
| #include <iostream> | |
| #include <memory> | |
| #include <optional> | |
| #include <regex> | |
| #include <sstream> | |
| #include <string> | |
| #include <string_view> | |
| #include <system_error> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/container/flat_hash_map.h" | |
| #include "src/dawn/common/SystemUtils.h" | |
| #include "src/dawn/node/napi_v8/napi_v8.h" | |
| #include "src/dawn/node/standalone/EventLoop.h" | |
| namespace dawn::node::standalone { | |
| namespace { | |
| // State shared by the polyfills that outlives registration. Owned by the `process` object, which | |
| // deletes it from its finalizer. | |
| struct PolyfillContext { | |
| struct RequireContext { | |
| PolyfillContext* ctx; | |
| std::string from_dir; | |
| }; | |
| PolyfillContext(EventLoop& event_loop, PolyfillOptions opts) | |
| : loop(event_loop), options(std::move(opts)) {} | |
| EventLoop& loop; | |
| PolyfillOptions options; | |
| std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now(); | |
| absl::flat_hash_map<std::string, Napi::ObjectReference> module_cache; | |
| std::vector<std::unique_ptr<RequireContext>> require_contexts; | |
| }; | |
| void DeletePolyfillContext(const Napi::Env&, PolyfillContext* ctx) { | |
| delete ctx; | |
| } | |
| // Joins the call's arguments with spaces, as the console methods display them. | |
| Napi::Value FormatArgs(const Napi::CallbackInfo& info) { | |
| std::stringstream ss; | |
| for (size_t i = 0; i < info.Length(); ++i) { | |
| if (i > 0) { | |
| ss << " "; | |
| } | |
| ss << info[i].ToString().Utf8Value(); | |
| } | |
| return Napi::String::New(info.Env(), ss.str()); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // console | |
| // --------------------------------------------------------------------------- | |
| // Backs console.log(), console.info() and console.debug(). Node defines the latter two as | |
| // aliases of the first, all writing to stdout with no prefix: | |
| // https://nodejs.org/api/console.html#consoledebugdata-args | |
| Napi::Value ConsoleLog(const Napi::CallbackInfo& info) { | |
| std::cout << FormatArgs(info).As<Napi::String>().Utf8Value() << std::endl; | |
| return info.Env().Undefined(); | |
| } | |
| Napi::Value ConsoleWarn(const Napi::CallbackInfo& info) { | |
| std::clog << "[WARN] " << FormatArgs(info).As<Napi::String>().Utf8Value() << std::endl; | |
| return info.Env().Undefined(); | |
| } | |
| Napi::Value ConsoleError(const Napi::CallbackInfo& info) { | |
| std::cerr << "[ERROR] " << FormatArgs(info).As<Napi::String>().Utf8Value() << std::endl; | |
| return info.Env().Undefined(); | |
| } | |
| // https://developer.mozilla.org/en-US/docs/Web/API/console | |
| void RegisterConsole(Napi::Env env) { | |
| Napi::Object console = Napi::Object::New(env); | |
| console.Set("log", Napi::Function::New(env, ConsoleLog, "log")); | |
| console.Set("info", Napi::Function::New(env, ConsoleLog, "info")); | |
| console.Set("debug", Napi::Function::New(env, ConsoleLog, "debug")); | |
| console.Set("warn", Napi::Function::New(env, ConsoleWarn, "warn")); | |
| console.Set("error", Napi::Function::New(env, ConsoleError, "error")); | |
| env.Global().Set("console", console); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Paths | |
| // --------------------------------------------------------------------------- | |
| // Whether `c` can begin a UNC path on this platform. | |
| // | |
| // Windows accepts both "\\server\share" and "//server/share" as UNC. On POSIX only the backslash | |
| // form is treated as one, because a leading "//" is a legal POSIX path. | |
| bool BeginsUncPath(char c) { | |
| #if defined(_WIN32) | |
| return c == '/' || c == '\\'; | |
| #else | |
| return c == '\\'; | |
| #endif | |
| } | |
| // Returns true if the path begins with two separators. That is the whole test: anything beginning | |
| // that way is a UNC path ("\\server\share") or a device path ("\\?\C:\..." and "\\.\..."). | |
| bool IsUncPath(std::string_view path) { | |
| return path.size() >= 2 && BeginsUncPath(path[0]) && BeginsUncPath(path[1]); | |
| } | |
| // Throws a JavaScript exception if `path` is a UNC or device path. Returns true if it threw. | |
| // | |
| // These are deliberately unsupported. Node.js handles them very differently | |
| // than std::filesystem::path. To match Node.js, we would need to implement it | |
| // from scratch. We will not do that unless there is a request from the users. | |
| bool RejectUncPath(Napi::Env env, const std::string& path) { | |
| if (!IsUncPath(path)) { | |
| return false; | |
| } | |
| Napi::Error::New(env, "UNC and device paths are not supported: " + path) | |
| .ThrowAsJavaScriptException(); | |
| return true; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // fs | |
| // --------------------------------------------------------------------------- | |
| // Retrieves the path argument from `info` and stores it in `*path`. Every fs | |
| // entry point with a path argument has it in the first position. If the path | |
| // cannot be retrieved or it is a UNC path, a JavaScript exception is thrown. | |
| bool GetPathArgument(const Napi::CallbackInfo& info, std::string* path) { | |
| if (info.Length() < 1 || !info[0].IsString()) { | |
| Napi::TypeError::New(info.Env(), "String expected for path").ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| *path = info[0].As<Napi::String>().Utf8Value(); | |
| return !RejectUncPath(info.Env(), *path); | |
| } | |
| // Which encoding the caller asked for. What kNone means is up to the caller: readFileSync() | |
| // answers with bytes, readdirSync() with UTF-8 strings, matching Node's defaults. | |
| enum class Encoding { | |
| kNone, | |
| kUtf8, | |
| }; | |
| // Reads the options argument the fs entry points take, which may be an encoding on its own or an | |
| // object holding one. | |
| // | |
| // Only UTF-8 is implemented and `encoding` is the only option understood. Anything else throws a | |
| // JavaScript exception. | |
| bool GetEncodingOption(const Napi::CallbackInfo& info, Encoding* encoding) { | |
| Napi::Env env = info.Env(); | |
| *encoding = Encoding::kNone; | |
| if (info.Length() < 2 || info[1].IsUndefined() || info[1].IsNull()) { | |
| return true; | |
| } | |
| Napi::Value value = info[1]; | |
| if (value.IsObject()) { | |
| Napi::Object options = value.As<Napi::Object>(); | |
| Napi::Array names = options.GetPropertyNames(); | |
| for (uint32_t i = 0; i < names.Length(); ++i) { | |
| std::string name = names.Get(i).ToString().Utf8Value(); | |
| if (name != "encoding") { | |
| Napi::Error::New(env, "Unsupported fs option: " + name) | |
| .ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| } | |
| value = options.Get("encoding"); | |
| if (value.IsUndefined() || value.IsNull()) { | |
| return true; | |
| } | |
| } | |
| if (!value.IsString()) { | |
| Napi::TypeError::New(env, "String expected for encoding").ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| std::string name = value.As<Napi::String>().Utf8Value(); | |
| std::string lower_name = name; | |
| for (char& c : lower_name) { | |
| c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); | |
| } | |
| // Node.js documentation specifies that all buffer encodings are case-insensitive. | |
| if (lower_name != "utf8" && lower_name != "utf-8") { | |
| Napi::Error::New(env, "Unsupported fs encoding: " + name).ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| *encoding = Encoding::kUtf8; | |
| return true; | |
| } | |
| bool OpenAndSizeFile(Napi::Env env, | |
| const std::string& path, | |
| std::ifstream* file, | |
| std::streamoff* size) { | |
| // Node.js reads the file in binary regardless of the encoding. | |
| file->open(path, std::ios::binary | std::ios::ate); | |
| if (!file->is_open()) { | |
| Napi::Error::New(env, "Failed to open file: " + path).ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| *size = file->tellg(); | |
| if (*size < 0) { | |
| Napi::Error::New(env, "Failed to size file: " + path).ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| file->seekg(0, std::ios::beg); | |
| return true; | |
| } | |
| bool ReadFileUtf8(Napi::Env env, const std::string& path, std::string* out) { | |
| std::ifstream file; | |
| std::streamoff offset = 0; | |
| if (!OpenAndSizeFile(env, path, &file, &offset)) { | |
| return false; | |
| } | |
| out->resize(static_cast<size_t>(offset)); | |
| file.read(out->data(), static_cast<std::streamsize>(offset)); | |
| out->resize(static_cast<size_t>(file.gcount())); | |
| return true; | |
| } | |
| Napi::Value ReadFileSync(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| std::string path; | |
| Encoding encoding = Encoding::kNone; | |
| if (!GetPathArgument(info, &path) || !GetEncodingOption(info, &encoding)) { | |
| return env.Undefined(); | |
| } | |
| if (encoding == Encoding::kUtf8) { | |
| std::string content; | |
| if (!ReadFileUtf8(env, path, &content)) { | |
| return env.Undefined(); | |
| } | |
| return Napi::String::New(env, content); | |
| } | |
| std::ifstream file; | |
| std::streamoff offset = 0; | |
| if (!OpenAndSizeFile(env, path, &file, &offset)) { | |
| return env.Undefined(); | |
| } | |
| // Read straight into the object being returned, so the contents are written once. | |
| // Resize to gcount() in case the file shrank between the seek and the read. | |
| Napi::ArrayBuffer array_buffer = Napi::ArrayBuffer::New(env, static_cast<size_t>(offset)); | |
| file.read(static_cast<char*>(array_buffer.Data()), static_cast<std::streamsize>(offset)); | |
| return Napi::Uint8Array::New(env, static_cast<size_t>(file.gcount()), array_buffer, 0); | |
| } | |
| // Returns true if the given file exists. A JavaScript exception is thrown if a | |
| // UNC path is given. | |
| Napi::Value ExistsSync(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| if (info.Length() < 1 || !info[0].IsString()) { | |
| return Napi::Boolean::New(env, false); | |
| } | |
| std::string path = info[0].As<Napi::String>().Utf8Value(); | |
| if (RejectUncPath(env, path)) { | |
| return env.Undefined(); | |
| } | |
| std::error_code ec; | |
| bool exists = std::filesystem::exists(path, ec); | |
| return Napi::Boolean::New(env, exists && !ec); | |
| } | |
| Napi::Value ReaddirSync(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| std::string path; | |
| // The entries are always strings, so the encoding needs validating rather than honouring; the | |
| // point of the call is to reject `withFileTypes` and friends. | |
| Encoding encoding = Encoding::kNone; | |
| if (!GetPathArgument(info, &path) || !GetEncodingOption(info, &encoding)) { | |
| return env.Undefined(); | |
| } | |
| std::error_code ec; | |
| std::filesystem::directory_iterator it(path, ec); | |
| if (ec) { | |
| Napi::Error::New(env, ec.message()).ThrowAsJavaScriptException(); | |
| return env.Undefined(); | |
| } | |
| Napi::Array result = Napi::Array::New(env); | |
| uint32_t index = 0; | |
| for (const auto& entry : it) { | |
| result.Set(index++, Napi::String::New(env, entry.path().filename().string())); | |
| } | |
| return result; | |
| } | |
| Napi::Value ReturnTrue(const Napi::CallbackInfo& info) { | |
| return Napi::Boolean::New(info.Env(), true); | |
| } | |
| Napi::Value ReturnFalse(const Napi::CallbackInfo& info) { | |
| return Napi::Boolean::New(info.Env(), false); | |
| } | |
| Napi::Value StatSync(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| std::string path; | |
| Encoding encoding = Encoding::kNone; | |
| if (!GetPathArgument(info, &path) || !GetEncodingOption(info, &encoding)) { | |
| return env.Undefined(); | |
| } | |
| std::error_code ec; | |
| std::filesystem::file_status status = std::filesystem::status(path, ec); | |
| if (ec || !std::filesystem::exists(status)) { | |
| Napi::Error::New(env, "Failed to stat: " + path).ThrowAsJavaScriptException(); | |
| return env.Undefined(); | |
| } | |
| // The answer is settled here, so each predicate is just the constant it will always return. | |
| Napi::Object stats = Napi::Object::New(env); | |
| stats.Set( | |
| "isFile", | |
| Napi::Function::New( | |
| env, std::filesystem::is_regular_file(status) ? ReturnTrue : ReturnFalse, "isFile")); | |
| stats.Set( | |
| "isDirectory", | |
| Napi::Function::New(env, std::filesystem::is_directory(status) ? ReturnTrue : ReturnFalse, | |
| "isDirectory")); | |
| return stats; | |
| } | |
| // https://nodejs.org/api/fs.html | |
| // | |
| // Only the synchronous entry points are native. The callback and promise forms are built on top of | |
| // them by the bootstrap script. | |
| void RegisterFs(Napi::Env env) { | |
| Napi::Object fs = Napi::Object::New(env); | |
| fs.Set("readFileSync", Napi::Function::New(env, ReadFileSync, "readFileSync")); | |
| fs.Set("existsSync", Napi::Function::New(env, ExistsSync, "existsSync")); | |
| fs.Set("readdirSync", Napi::Function::New(env, ReaddirSync, "readdirSync")); | |
| fs.Set("statSync", Napi::Function::New(env, StatSync, "statSync")); | |
| env.Global().Set("_fs_polyfill", fs); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // path | |
| // --------------------------------------------------------------------------- | |
| // | |
| // Built on std::filesystem, which walks the components and, on Windows, understands drive letters | |
| // and accepts either separator. Its conventions are not Node's, so each function below lets | |
| // std::filesystem handle the general case and then fixes up the places where the two disagree. | |
| // | |
| // UNC and device paths are refused rather than reconciled; see RejectUncPath. | |
| constexpr char kPreferredSeparator = static_cast<char>(std::filesystem::path::preferred_separator); | |
| // What counts as a separator on this host. Windows accepts either; POSIX only the forward slash. | |
| #if defined(_WIN32) | |
| constexpr std::string_view kSeparators = "/\\"; | |
| #else | |
| constexpr std::string_view kSeparators = "/"; | |
| #endif | |
| bool IsSeparator(char c) { | |
| return kSeparators.find(c) != std::string_view::npos; | |
| } | |
| // A trailing separator is a flag to Node but a real, empty final component to std::filesystem, so | |
| // filename(), extension() and parent_path() all see one component more than Node does. Dropping it | |
| // first is what makes them agree. The root is never stripped: "/" and "C:\" are not trailing | |
| // separators. | |
| std::string TrimTrailingSeparators(const std::filesystem::path& path) { | |
| const std::string text = path.string(); | |
| const size_t root = path.root_path().string().size(); | |
| const size_t last = text.find_last_not_of(kSeparators); | |
| const size_t end = last == std::string::npos ? root : std::max(root, last + 1); | |
| return text.substr(0, end); | |
| } | |
| // Two or more leading separators collapse to one, which is what Node does on POSIX. On Windows two | |
| // separators would mean UNC, and those are refused at the boundary, so anything arriving here with | |
| // the shape is an ordinary rooted path that concatenation happened to produce - join('/', 'a') | |
| // builds "/\a" on the way to "\a". | |
| std::string CollapseLeadingSeparators(std::string text) { | |
| size_t run = text.find_first_not_of(kSeparators); | |
| if (run == std::string::npos) { | |
| run = text.size(); // nothing but separators | |
| } | |
| if (run > 1) { | |
| text.erase(0, run - 1); | |
| } | |
| return text; | |
| } | |
| std::string NormalizePath(const std::string& input) { | |
| if (input.empty()) { | |
| return "."; // lexically_normal() returns "" | |
| } | |
| const std::filesystem::path path(input); | |
| // lexically_normal() both drops a trailing separator ("./" becomes ".") and invents one | |
| // ("a/b/.." becomes "a/"), so neither its presence nor its absence in the output means | |
| // anything. Taking it off and putting it back according to the input is what Node does. | |
| const bool trailing = IsSeparator(input.back()); | |
| std::string result = TrimTrailingSeparators(path.lexically_normal()); | |
| if (result.empty()) { | |
| result = "."; | |
| } else if (path.has_root_name() && !path.has_root_directory() && | |
| result == path.root_name().string()) { | |
| // A path like "C:" on Windows implies the "." directory on that drive. | |
| // Node makes this explicit by turning it into "C:.". | |
| // This cannot happen for POSIX. | |
| result += '.'; | |
| } | |
| if (trailing && !IsSeparator(result.back())) { | |
| result += kPreferredSeparator; | |
| } | |
| return CollapseLeadingSeparators(result); | |
| } | |
| // The one entry point not expressible in terms of a std::filesystem primitive. Node's dirname() is | |
| // a raw string slice that deliberately does not normalize - dirname("/a//b") is "/a/", separator | |
| // run intact - while parent_path() is component-based and has already discarded that. So Node's | |
| // scan is transcribed, with root_path() supplying the one genuinely platform-specific part: where | |
| // the root ends, be that "/" or "C:\". | |
| std::string DirnamePath(const std::string& input) { | |
| if (input.empty()) { | |
| return "."; | |
| } | |
| const size_t root_length = std::filesystem::path(input).root_path().string().size(); | |
| const bool has_root = root_length > 0; | |
| // Search for the last separator, ignoring any trailing separators and the | |
| // root. Separators in the root have a special meaning. The result is | |
| // everything that appears before the separator preceding the last component. | |
| const std::string_view path_without_root = std::string_view(input).substr(root_length); | |
| // Skip any trailing separators to locate the end of the last path component. | |
| const size_t last_non_separator = path_without_root.find_last_not_of(kSeparators); | |
| // Find the separator immediately preceding the last component. | |
| const size_t separator_before_last_component = | |
| last_non_separator == std::string_view::npos | |
| ? std::string_view::npos | |
| : path_without_root.find_last_of(kSeparators, last_non_separator); | |
| if (separator_before_last_component == std::string_view::npos) { | |
| // Nothing to cut back to, so the answer is the root, or "." when there is no root. | |
| return has_root ? input.substr(0, root_length) : "."; | |
| } | |
| // Find the index of the separator in the original string. | |
| const size_t last_separator_index = root_length + separator_before_last_component; | |
| /* | |
| * Node's posix dirname() has exactly one special case, quoted from Node's implementation: | |
| * | |
| * // POSIX reserves a leading '//' for implementation-defined purposes. | |
| * // (IEEE Std 1003.1-2017, Section 4.13 Pathname Resolution: | |
| * // "A pathname that begins with two successive slashes may be interpreted | |
| * // in an implementation-defined manner, although more than two leading | |
| * // slashes shall be treated as a single slash.") | |
| * // Node keeps both slashes: dirname("//a") is "//" rather than "/". | |
| * | |
| * On Windows, leading double slashes represent a UNC path, which is rejected | |
| * earlier before reaching this function. | |
| */ | |
| if (has_root && last_separator_index == 1) { | |
| return input.substr(0, 2); | |
| } | |
| // Return the substring up to, but not including, the separator before the last component. | |
| return input.substr(0, last_separator_index); | |
| } | |
| std::string JoinPaths(const std::vector<std::string>& args) { | |
| // std::filesystem::path::append cannot be used here. If one of `args` is an | |
| // absolute path, Node will join them: {"/a", "/b"} -> "/a/b". However, | |
| // std::filesystem::path drops the previous paths: {"/a", "/b"} -> "/b". | |
| std::string joined; | |
| for (const std::string& arg : args) { | |
| if (arg.empty()) { | |
| continue; // Node skips empty arguments rather than reading them as "." | |
| } | |
| if (!joined.empty()) { | |
| joined += kPreferredSeparator; | |
| } | |
| joined += arg; | |
| } | |
| if (joined.empty()) { | |
| return "."; | |
| } | |
| return NormalizePath(joined); | |
| } | |
| // The working directory, or false with an exception pending. Shared by process.cwd() and by | |
| // resolve(), which anchors relative arguments on it. | |
| bool CurrentDirectory(Napi::Env env, std::string* out) { | |
| std::error_code ec; | |
| const std::filesystem::path cwd = std::filesystem::current_path(ec); | |
| if (ec) { | |
| Napi::Error::New(env, "Could not read the working directory: " + ec.message()) | |
| .ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| *out = cwd.string(); | |
| // A UNC working directory means a checkout on a network share. Everything resolve() produces | |
| // would be built on it, so it fails here rather than somewhere downstream that gives no hint | |
| // of the cause. | |
| return !RejectUncPath(env, *out); | |
| } | |
| bool ResolvePaths(Napi::Env env, const std::vector<std::string>& args, std::string* out) { | |
| std::filesystem::path accumulated; | |
| for (const std::string& arg : args) { | |
| if (arg.empty()) { | |
| continue; // absolute("") fails with EINVAL, and Node ignores empty arguments | |
| } | |
| // Here operator/ is exactly right: its rule that a later absolute argument replaces | |
| // everything before it, including the Windows rule that a rooted argument keeps the | |
| // left-hand side's drive, is what resolve() specifies. | |
| accumulated /= std::filesystem::path(arg); | |
| } | |
| // The working directory is read here rather than left to absolute() so that a working | |
| // directory that cannot be read, or that is UNC, is reported as such instead of surfacing as a | |
| // confusing failure about the argument. | |
| if (!accumulated.is_absolute()) { | |
| std::string cwd; | |
| if (!CurrentDirectory(env, &cwd)) { | |
| return false; | |
| } | |
| // If `accumulated` is empty, the call to std::filesystem::absolute | |
| // below will fail. Replacing it with `cwd` to match Node's behaviour. | |
| if (accumulated.empty()) { | |
| accumulated = std::filesystem::path(cwd); | |
| } | |
| } | |
| // absolute() rather than `cwd / accumulated`: on Windows a drive-relative argument like "C:a" | |
| // names the working directory of that drive, which only absolute() knows how to consult - | |
| // operator/ would see a root-name of its own and keep "C:a" relative. On POSIX the two are the | |
| // same thing. | |
| std::error_code ec; | |
| const std::filesystem::path absolute = std::filesystem::absolute(accumulated, ec); | |
| if (ec) { | |
| Napi::Error::New(env, "Could not resolve path: " + ec.message()) | |
| .ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| // Node's resolve() never returns a trailing separator, but lexically_normal() produces one | |
| // whenever the last component was a dot segment. | |
| std::string result = TrimTrailingSeparators(absolute.lexically_normal()); | |
| if (result.empty()) { | |
| result = "."; | |
| } | |
| *out = CollapseLeadingSeparators(result); | |
| return true; | |
| } | |
| bool RelativePath(Napi::Env env, const std::string& from, const std::string& to, std::string* out) { | |
| // Node resolves both sides first, so the answer depends only on where they land. | |
| std::string from_resolved; | |
| std::string to_resolved; | |
| if (!ResolvePaths(env, {from}, &from_resolved) || !ResolvePaths(env, {to}, &to_resolved)) { | |
| return false; | |
| } | |
| const std::filesystem::path result = | |
| std::filesystem::path(to_resolved).lexically_relative(std::filesystem::path(from_resolved)); | |
| if (result == std::filesystem::path(".")) { | |
| *out = ""; // lexically_relative() says "." for identical paths; Node says "" | |
| } else if (result.empty()) { | |
| *out = to_resolved; // unrelated roots, where Node falls back to the resolved `to` | |
| } else { | |
| *out = result.string(); | |
| } | |
| return true; | |
| } | |
| // Reads the string arguments a path entry point takes, throwing the TypeError Node throws for a | |
| // non-string, and refusing UNC paths. | |
| bool GetPathArguments(const Napi::CallbackInfo& info, | |
| size_t least, | |
| std::vector<std::string>* args) { | |
| if (info.Length() < least) { | |
| Napi::TypeError::New(info.Env(), "String expected for path").ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| for (size_t i = 0; i < info.Length(); ++i) { | |
| if (!info[i].IsString()) { | |
| Napi::TypeError::New(info.Env(), "String expected for path") | |
| .ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| std::string arg = info[i].As<Napi::String>().Utf8Value(); | |
| if (RejectUncPath(info.Env(), arg)) { | |
| return false; | |
| } | |
| args->push_back(std::move(arg)); | |
| } | |
| return true; | |
| } | |
| Napi::Value PathNormalize(const Napi::CallbackInfo& info) { | |
| std::vector<std::string> args; | |
| if (!GetPathArguments(info, 1, &args)) { | |
| return info.Env().Undefined(); | |
| } | |
| return Napi::String::New(info.Env(), NormalizePath(args[0])); | |
| } | |
| Napi::Value PathDirname(const Napi::CallbackInfo& info) { | |
| std::vector<std::string> args; | |
| if (!GetPathArguments(info, 1, &args)) { | |
| return info.Env().Undefined(); | |
| } | |
| return Napi::String::New(info.Env(), DirnamePath(args[0])); | |
| } | |
| Napi::Value PathJoin(const Napi::CallbackInfo& info) { | |
| std::vector<std::string> args; | |
| if (!GetPathArguments(info, 0, &args)) { | |
| return info.Env().Undefined(); | |
| } | |
| return Napi::String::New(info.Env(), JoinPaths(args)); | |
| } | |
| Napi::Value PathResolve(const Napi::CallbackInfo& info) { | |
| std::vector<std::string> args; | |
| std::string resolved; | |
| if (!GetPathArguments(info, 0, &args) || !ResolvePaths(info.Env(), args, &resolved)) { | |
| return info.Env().Undefined(); | |
| } | |
| return Napi::String::New(info.Env(), resolved); | |
| } | |
| Napi::Value PathRelative(const Napi::CallbackInfo& info) { | |
| std::vector<std::string> args; | |
| std::string relative; | |
| if (!GetPathArguments(info, 2, &args) || | |
| !RelativePath(info.Env(), args[0], args[1], &relative)) { | |
| return info.Env().Undefined(); | |
| } | |
| return Napi::String::New(info.Env(), relative); | |
| } | |
| // https://nodejs.org/api/path.html | |
| void RegisterPath(Napi::Env env) { | |
| Napi::Object path = Napi::Object::New(env); | |
| path.Set("dirname", Napi::Function::New(env, PathDirname, "dirname")); | |
| path.Set("join", Napi::Function::New(env, PathJoin, "join")); | |
| path.Set("normalize", Napi::Function::New(env, PathNormalize, "normalize")); | |
| path.Set("relative", Napi::Function::New(env, PathRelative, "relative")); | |
| path.Set("resolve", Napi::Function::New(env, PathResolve, "resolve")); | |
| path.Set("sep", Napi::String::New(env, std::string(1, kPreferredSeparator))); | |
| env.Global().Set("_path_polyfill", path); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // process | |
| // --------------------------------------------------------------------------- | |
| Napi::Value Cwd(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| // The working directory is the one path the runtime is not handed by its caller, so it is also | |
| // the one that can go wrong without anybody having asked for anything unusual. Everything | |
| // resolve() produces is built on it, so CurrentDirectory() throws rather than return a string | |
| // that would quietly contaminate every path derived from it. | |
| std::string cwd; | |
| if (!CurrentDirectory(env, &cwd)) { | |
| return env.Undefined(); | |
| } | |
| return Napi::String::New(env, cwd); | |
| } | |
| Napi::Value Exit(const Napi::CallbackInfo& info) { | |
| auto* ctx = static_cast<PolyfillContext*>(info.Data()); | |
| int32_t code = 0; | |
| if (info.Length() > 0 && info[0].IsNumber()) { | |
| code = info[0].As<Napi::Number>().Int32Value(); | |
| } | |
| ctx->loop.Stop(code); | |
| // Unwind the running JavaScript frames with an uncatchable termination exception so execution | |
| // does not continue after process.exit(). | |
| napi_env c_env = info.Env(); | |
| reinterpret_cast<napi_env__*>(c_env)->isolate->TerminateExecution(); | |
| return info.Env().Undefined(); | |
| } | |
| Napi::Value HrtimeBigint(const Napi::CallbackInfo& info) { | |
| auto now = std::chrono::steady_clock::now().time_since_epoch(); | |
| int64_t nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(now).count(); | |
| return Napi::BigInt::New(info.Env(), nanos); | |
| } | |
| // https://nodejs.org/api/stream.html#writablewritechunk-encoding-callback | |
| // | |
| // write() returns whether the caller may continue writing immediately, or should wait for a | |
| // 'drain' event because the stream buffered the chunk. These writes go straight to the underlying | |
| // stream and buffer nothing, so the answer is always true. | |
| Napi::Value StdoutWrite(const Napi::CallbackInfo& info) { | |
| if (info.Length() > 0) { | |
| std::cout << info[0].ToString().Utf8Value() << std::flush; | |
| } | |
| return Napi::Boolean::New(info.Env(), true); | |
| } | |
| Napi::Value StderrWrite(const Napi::CallbackInfo& info) { | |
| if (info.Length() > 0) { | |
| std::clog << info[0].ToString().Utf8Value() << std::flush; | |
| } | |
| return Napi::Boolean::New(info.Env(), true); | |
| } | |
| // https://nodejs.org/api/process.html | |
| void RegisterProcess(Napi::Env env, const PolyfillOptions& options, PolyfillContext* ctx) { | |
| Napi::Object process = Napi::Object::New(env); | |
| process.Set("cwd", Napi::Function::New(env, Cwd, "cwd")); | |
| process.Set("exit", Napi::Function::New(env, Exit, "exit", ctx)); | |
| Napi::Object hrtime = Napi::Object::New(env); | |
| hrtime.Set("bigint", Napi::Function::New(env, HrtimeBigint, "bigint")); | |
| process.Set("hrtime", hrtime); | |
| // Libraries detect a Node-like environment via `process?.versions?.node !== undefined`; the | |
| // version string itself is not inspected. | |
| Napi::Object versions = Napi::Object::New(env); | |
| versions.Set("node", Napi::String::New(env, "0.0.0")); | |
| process.Set("versions", versions); | |
| Napi::Object env_obj = Napi::Object::New(env); | |
| if (auto [dawn_flags, is_set] = dawn::GetEnvironmentVar("DAWN_FLAGS"); is_set) { | |
| env_obj.Set("DAWN_FLAGS", Napi::String::New(env, dawn_flags)); | |
| } | |
| process.Set("env", env_obj); | |
| Napi::Array argv_array = Napi::Array::New(env, options.argv.size()); | |
| for (uint32_t i = 0; i < options.argv.size(); ++i) { | |
| argv_array.Set(i, Napi::String::New(env, options.argv[i])); | |
| } | |
| process.Set("argv", argv_array); | |
| Napi::Object stdout_obj = Napi::Object::New(env); | |
| stdout_obj.Set("write", Napi::Function::New(env, StdoutWrite)); | |
| process.Set("stdout", stdout_obj); | |
| Napi::Object stderr_obj = Napi::Object::New(env); | |
| stderr_obj.Set("write", Napi::Function::New(env, StderrWrite)); | |
| process.Set("stderr", stderr_obj); | |
| // `process` owns the context: this releases it once the object is collected. | |
| process.AddFinalizer(DeletePolyfillContext, ctx); | |
| env.Global().Set("process", process); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Scheduling / Timers (setImmediate, setTimeout, clearTimeout) | |
| // --------------------------------------------------------------------------- | |
| // Captures the arguments a timer callback is to be called with: everything from `first` onwards. | |
| std::vector<Napi::Reference<Napi::Value>> CaptureArgs(const Napi::CallbackInfo& info, | |
| size_t first) { | |
| std::vector<Napi::Reference<Napi::Value>> args; | |
| args.reserve(info.Length() > first ? info.Length() - first : 0); | |
| for (size_t i = first; i < info.Length(); ++i) { | |
| args.push_back(Napi::Persistent(info[i])); | |
| } | |
| return args; | |
| } | |
| // Reads captured arguments back out for a call. | |
| std::vector<napi_value> ResolveArgs(const std::vector<Napi::Reference<Napi::Value>>& args) { | |
| std::vector<napi_value> values; | |
| values.reserve(args.size()); | |
| for (const auto& arg : args) { | |
| values.push_back(arg.Value()); | |
| } | |
| return values; | |
| } | |
| // Implements setImmediate(). Node runs these in the check phase of its event loop, so the task is | |
| // handed to the embedder rather than run here; the embedder decides when the check phase comes | |
| // around. V8 has no queue of its own to use instead: macrotasks are not an ECMAScript concept, and | |
| // the only queue V8 owns is the microtask queue that backs promise reactions. | |
| // | |
| // Node returns an Immediate object (with ref(), unref() and clearImmediate() cancellation). The | |
| // loop has no cancellation for check-phase tasks and nothing in the CTS or Dawn bindings uses the | |
| // return value, so this returns undefined and clearImmediate is not registered. | |
| Napi::Value SetImmediate(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| if (info.Length() < 1 || !info[0].IsFunction()) { | |
| Napi::TypeError::New(env, "Function expected for setImmediate") | |
| .ThrowAsJavaScriptException(); | |
| return env.Undefined(); | |
| } | |
| auto* ctx = static_cast<PolyfillContext*>(info.Data()); | |
| Napi::FunctionReference fn = Napi::Persistent(info[0].As<Napi::Function>()); | |
| // Any arguments beyond the callback are forwarded to it. | |
| std::vector<Napi::Reference<Napi::Value>> args = CaptureArgs(info, 1); | |
| ctx->loop.PostTask( | |
| [fn = std::move(fn), args = std::move(args)]() mutable { fn.Call(ResolveArgs(args)); }); | |
| return env.Undefined(); | |
| } | |
| // Converts a JavaScript delay, which is a possibly fractional count of milliseconds, to the | |
| // duration the loop measures in. | |
| EventLoop::Duration DelayFromMilliseconds(double delay_ms) { | |
| return std::chrono::duration_cast<EventLoop::Duration>( | |
| std::chrono::duration<double, std::milli>(delay_ms)); | |
| } | |
| // Implements setTimeout(). Returns the EventLoop::TimerId as a number to pass to clearTimeout(). | |
| // Node.js returns a Timeout object, but a numeric handle - as on the web - is all the CTS uses. | |
| Napi::Value SetTimeout(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| if (info.Length() < 1 || !info[0].IsFunction()) { | |
| Napi::TypeError::New(env, "Function expected for setTimeout").ThrowAsJavaScriptException(); | |
| return env.Undefined(); | |
| } | |
| double delay_ms = 0.0; | |
| if (info.Length() > 1 && info[1].IsNumber()) { | |
| delay_ms = info[1].As<Napi::Number>().DoubleValue(); | |
| } | |
| // Match the web platform: negative, NaN and missing delays are treated as zero. | |
| if (!(delay_ms > 0.0)) { | |
| delay_ms = 0.0; | |
| } | |
| auto* ctx = static_cast<PolyfillContext*>(info.Data()); | |
| Napi::FunctionReference fn = Napi::Persistent(info[0].As<Napi::Function>()); | |
| std::vector<Napi::Reference<Napi::Value>> args = CaptureArgs(info, 2); | |
| const EventLoop::TimerId id = ctx->loop.PostDelayedTask( | |
| [fn = std::move(fn), args = std::move(args)]() mutable { fn.Call(ResolveArgs(args)); }, | |
| DelayFromMilliseconds(delay_ms)); | |
| return Napi::Number::New(env, static_cast<double>(id)); | |
| } | |
| // Implements clearTimeout(). Clearing an unknown or already-fired handle is not an error, as | |
| // required by the standard. | |
| Napi::Value ClearTimeout(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| auto* ctx = static_cast<PolyfillContext*>(info.Data()); | |
| if (ctx == nullptr || info.Length() < 1 || !info[0].IsNumber()) { | |
| return env.Undefined(); | |
| } | |
| const auto id = static_cast<EventLoop::TimerId>(info[0].As<Napi::Number>().DoubleValue()); | |
| ctx->loop.CancelDelayedTask(id); | |
| return env.Undefined(); | |
| } | |
| void RegisterTimers(Napi::Env env, PolyfillContext* ctx) { | |
| env.Global().Set("setImmediate", Napi::Function::New(env, SetImmediate, "setImmediate", ctx)); | |
| env.Global().Set("setTimeout", Napi::Function::New(env, SetTimeout, "setTimeout", ctx)); | |
| env.Global().Set("clearTimeout", Napi::Function::New(env, ClearTimeout, "clearTimeout", ctx)); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // performance | |
| // --------------------------------------------------------------------------- | |
| Napi::Value PerformanceNow(const Napi::CallbackInfo& info) { | |
| auto* ctx = static_cast<PolyfillContext*>(info.Data()); | |
| auto now = std::chrono::steady_clock::now(); | |
| double millis = 0.0; | |
| if (ctx != nullptr) { | |
| millis = std::chrono::duration<double, std::milli>(now - ctx->start_time).count(); | |
| } | |
| return Napi::Number::New(info.Env(), millis); | |
| } | |
| // https://developer.mozilla.org/en-US/docs/Web/API/Performance/now | |
| void RegisterPerformance(Napi::Env env, PolyfillContext* ctx) { | |
| Napi::Object performance = Napi::Object::New(env); | |
| performance.Set("now", Napi::Function::New(env, PerformanceNow, "now", ctx)); | |
| env.Global().Set("performance", performance); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // TextEncoder | |
| // --------------------------------------------------------------------------- | |
| void TextEncoderConstructor(const Napi::CallbackInfo&) {} | |
| // Implements TextEncoder.prototype.encode(). V8 holds strings as UTF-16, and its UTF-8 conversion | |
| // already implements the WHATWG encode algorithm: unpaired surrogates become U+FFFD rather than an | |
| // error. | |
| Napi::Value EncodeUtf8(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| std::string utf8; | |
| if (info.Length() > 0 && !info[0].IsUndefined()) { | |
| utf8 = info[0].ToString().Utf8Value(); | |
| } | |
| Napi::Uint8Array out = Napi::Uint8Array::New(env, utf8.size()); | |
| std::ranges::copy(utf8, out.Data()); | |
| return out; | |
| } | |
| void RegisterTextEncoder(Napi::Env env) { | |
| // Only encode() is implemented. | |
| Napi::Function ctor = Napi::Function::New(env, TextEncoderConstructor, "TextEncoder"); | |
| ctor.Get("prototype") | |
| .As<Napi::Object>() | |
| .Set("encode", Napi::Function::New(env, EncodeUtf8, "encode")); | |
| env.Global().Set("TextEncoder", ctor); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // require | |
| // --------------------------------------------------------------------------- | |
| std::optional<Napi::Value> TryGetBuiltinModule(Napi::Env env, std::string_view specifier) { | |
| if (specifier.starts_with("node:")) { | |
| specifier.remove_prefix(5); | |
| } | |
| Napi::Object global = env.Global(); | |
| if (specifier == "fs") { | |
| return global.Get("_fs_polyfill"); | |
| } | |
| if (specifier == "path") { | |
| return global.Get("_path_polyfill"); | |
| } | |
| if (specifier == "process") { | |
| return global.Get("process"); | |
| } | |
| if (specifier == "perf_hooks") { | |
| Napi::Object perf_hooks = Napi::Object::New(env); | |
| perf_hooks.Set("performance", global.Get("performance")); | |
| return perf_hooks; | |
| } | |
| // `dawn.node` is statically linked into the runner binary and registered on | |
| // `globalThis._webgpu_module`. | |
| if (std::filesystem::path(specifier).filename() == "dawn.node") { | |
| return global.Get("_webgpu_module"); | |
| } | |
| return std::nullopt; | |
| } | |
| // Returns true if `specifier` is a CommonJS bare package name (such as "ansi-colors" or | |
| // "pkg/subpath") rather than a relative or absolute filesystem path. | |
| bool IsBarePackageName(std::string_view specifier) { | |
| // If `specifier` starts with an explicit relative path (like `.` or `..`) or a root slash, it | |
| // is not a bare package name. | |
| static const std::regex kPathPrefix(R"(^(\.\.?$|\.*[/\\]))"); | |
| if (std::regex_search(specifier.begin(), specifier.end(), kPathPrefix)) { | |
| return false; | |
| } | |
| return !std::filesystem::path(specifier).is_absolute(); | |
| } | |
| // Returns a JavaScript function that takes parameters `(exports, require, module, __filename, | |
| // __dirname)` and runs `source` as the body. `filename` is the name of the file the source came | |
| // from. Errors will be reported using their line number in `source` in the given `filename`. | |
| // | |
| // Note, this is different than `napi_run_script()`, which immediately executes a top-level script | |
| // and is not concerned with the file and line number for reporting errors, which is why they have | |
| // different implementations. | |
| Napi::Function CompileModuleFunction(Napi::Env env, | |
| const std::string& source, | |
| const std::string& filename) { | |
| napi_env c_env = env; | |
| v8::Isolate* isolate = c_env->isolate; | |
| v8::Local<v8::Context> context = c_env->GetContext(); | |
| v8::Local<v8::String> v8_source = | |
| dawn::napi_v8::ToV8(Napi::String::New(env, source)).As<v8::String>(); | |
| v8::Local<v8::String> v8_origin = | |
| dawn::napi_v8::ToV8(Napi::String::New(env, filename)).As<v8::String>(); | |
| v8::Local<v8::String> params[] = { | |
| v8::String::NewFromUtf8Literal(isolate, "exports"), | |
| v8::String::NewFromUtf8Literal(isolate, "require"), | |
| v8::String::NewFromUtf8Literal(isolate, "module"), | |
| v8::String::NewFromUtf8Literal(isolate, "__filename"), | |
| v8::String::NewFromUtf8Literal(isolate, "__dirname"), | |
| }; | |
| v8::ScriptOrigin origin(v8_origin); | |
| v8::ScriptCompiler::Source script_source(v8_source, origin); | |
| v8::MaybeLocal<v8::Function> function; | |
| v8::Local<v8::Value> exception; | |
| { | |
| v8::TryCatch try_catch(isolate); | |
| function = | |
| v8::ScriptCompiler::CompileFunction(context, &script_source, std::size(params), params); | |
| if (function.IsEmpty() || try_catch.HasCaught()) { | |
| exception = try_catch.Exception(); | |
| } | |
| } | |
| if (!exception.IsEmpty()) { | |
| napi_throw(c_env, dawn::napi_v8::ToNapi(exception)); | |
| return Napi::Function(); | |
| } | |
| return Napi::Function(env, dawn::napi_v8::ToNapi(function.ToLocalChecked())); | |
| } | |
| Napi::Function MakeRequire(Napi::Env env, PolyfillContext* ctx, std::string from_dir); | |
| Napi::Value LoadModule(Napi::Env env, PolyfillContext* ctx, const std::string& resolved_path) { | |
| auto cached = ctx->module_cache.find(resolved_path); | |
| if (cached != ctx->module_cache.end()) { | |
| return cached->second.Value().Get("exports"); | |
| } | |
| std::string content; | |
| if (!ReadFileUtf8(env, resolved_path, &content)) { | |
| return env.Undefined(); | |
| } | |
| std::string dirname = DirnamePath(resolved_path); | |
| Napi::Object module = Napi::Object::New(env); | |
| Napi::Object exports = Napi::Object::New(env); | |
| module.Set("exports", exports); | |
| module.Set("id", Napi::String::New(env, resolved_path)); | |
| module.Set("filename", Napi::String::New(env, resolved_path)); | |
| module.Set("path", Napi::String::New(env, dirname)); | |
| module.Set("loaded", Napi::Boolean::New(env, false)); | |
| // Cache before running the module body so circular require() calls receive the in-progress | |
| // exports object rather than re-entering LoadModule. | |
| ctx->module_cache.emplace(resolved_path, Napi::Persistent(module)); | |
| Napi::Function fn = CompileModuleFunction(env, content, resolved_path); | |
| if (fn.IsEmpty()) { | |
| ctx->module_cache.erase(resolved_path); | |
| return env.Undefined(); | |
| } | |
| Napi::Function local_require = MakeRequire(env, ctx, dirname); | |
| Napi::Value call_result = | |
| fn.Call({exports, local_require, module, Napi::String::New(env, resolved_path), | |
| Napi::String::New(env, dirname)}); | |
| if (call_result.IsEmpty()) { | |
| ctx->module_cache.erase(resolved_path); | |
| return env.Undefined(); | |
| } | |
| module.Set("loaded", Napi::Boolean::New(env, true)); | |
| return module.Get("exports"); | |
| } | |
| Napi::Value Require(const Napi::CallbackInfo& info) { | |
| Napi::Env env = info.Env(); | |
| if (info.Length() < 1 || !info[0].IsString()) { | |
| Napi::TypeError::New(env, "String expected for module specifier") | |
| .ThrowAsJavaScriptException(); | |
| return env.Undefined(); | |
| } | |
| std::string specifier = info[0].As<Napi::String>().Utf8Value(); | |
| if (RejectUncPath(env, specifier)) { | |
| return env.Undefined(); | |
| } | |
| if (std::optional<Napi::Value> builtin = TryGetBuiltinModule(env, specifier)) { | |
| return *builtin; | |
| } | |
| // Bare package names outside `TryGetBuiltinModule` are not resolved against `node_modules`. | |
| if (IsBarePackageName(specifier)) { | |
| Napi::Error::New(env, "Cannot find module '" + specifier + "'") | |
| .ThrowAsJavaScriptException(); | |
| return env.Undefined(); | |
| } | |
| auto* req_ctx = static_cast<PolyfillContext::RequireContext*>(info.Data()); | |
| std::string from_dir = req_ctx->from_dir; | |
| if (from_dir.empty() && !CurrentDirectory(env, &from_dir)) { | |
| return env.Undefined(); | |
| } | |
| std::string resolved; | |
| if (!ResolvePaths(env, {from_dir, specifier}, &resolved)) { | |
| return env.Undefined(); | |
| } | |
| return LoadModule(env, req_ctx->ctx, resolved); | |
| } | |
| Napi::Function MakeRequire(Napi::Env env, PolyfillContext* ctx, std::string from_dir) { | |
| auto& req_ctx = | |
| ctx->require_contexts.emplace_back(std::make_unique<PolyfillContext::RequireContext>( | |
| PolyfillContext::RequireContext{ctx, std::move(from_dir)})); | |
| return Napi::Function::New(env, Require, "require", req_ctx.get()); | |
| } | |
| // https://nodejs.org/api/modules.html | |
| void RegisterRequire(Napi::Env env, PolyfillContext* ctx) { | |
| env.Global().Set("require", MakeRequire(env, ctx, "")); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // bootstrap | |
| // --------------------------------------------------------------------------- | |
| const char* kBootstrapScript = R"bootstrap( | |
| (function() { | |
| // DOM / Web Event Globals. Only the fields and methods used by Dawn's bindings and the CTS are | |
| // implemented. | |
| class Event { | |
| constructor(type, eventInitDict) { | |
| this.type = type; | |
| this.cancelable = Boolean(eventInitDict && eventInitDict.cancelable); | |
| this.defaultPrevented = false; | |
| } | |
| preventDefault() { | |
| if (this.cancelable) { | |
| this.defaultPrevented = true; | |
| } | |
| } | |
| } | |
| class EventTarget { | |
| constructor() { | |
| // Keyed by `type` alone (not `(type, capture)`) so dispatchEvent runs listeners in | |
| // registration order. There will be at most 2 entries per listener and type (capture | |
| // true and false). | |
| this._listeners = {}; | |
| } | |
| addEventListener(type, listener, options) { | |
| if (!listener) { | |
| return; | |
| } | |
| if (!this._listeners[type]) { | |
| this._listeners[type] = []; | |
| } | |
| const capture = typeof options === 'boolean' | |
| ? options : Boolean(options && options.capture); | |
| if (this._listeners[type].some( | |
| e => e.listener === listener && e.capture === capture)) { | |
| return; | |
| } | |
| const once = Boolean(options && typeof options === 'object' && options.once); | |
| this._listeners[type].push({ listener, once, capture }); | |
| } | |
| removeEventListener(type, listener, options) { | |
| if (!this._listeners[type]) { | |
| return; | |
| } | |
| const capture = typeof options === 'boolean' | |
| ? options : Boolean(options && options.capture); | |
| this._listeners[type] = this._listeners[type].filter( | |
| e => e.listener !== listener || e.capture !== capture); | |
| } | |
| dispatchEvent(event) { | |
| const type = event.type; | |
| // Copied, since a listener may add or remove listeners while it runs - a 'once' | |
| // listener removes itself before being invoked. | |
| const entries = this._listeners[type] ? this._listeners[type].slice() : []; | |
| for (const entry of entries) { | |
| if (entry.once) { | |
| this.removeEventListener(type, entry.listener, entry.capture); | |
| } | |
| const listener = entry.listener; | |
| if (typeof listener === 'function') { | |
| listener.call(this, event); | |
| } else if (listener && typeof listener.handleEvent === 'function') { | |
| listener.handleEvent(event); | |
| } | |
| } | |
| // A dispatch reports whether the default action should still be taken. | |
| return !event.defaultPrevented; | |
| } | |
| } | |
| class DOMException extends Error { | |
| constructor(message, name) { | |
| super(message); | |
| this.name = name || 'Error'; | |
| } | |
| } | |
| class CustomEvent extends Event {} | |
| class MessageEvent extends Event { | |
| constructor(type, eventInitDict) { | |
| super(type, eventInitDict); | |
| this.data = eventInitDict ? eventInitDict.data : undefined; | |
| } | |
| } | |
| globalThis.Event = Event; | |
| globalThis.CustomEvent = CustomEvent; | |
| globalThis.EventTarget = EventTarget; | |
| globalThis.DOMException = DOMException; | |
| globalThis.MessageEvent = MessageEvent; | |
| // queueMicrotask | |
| if (typeof globalThis.queueMicrotask !== 'function') { | |
| globalThis.queueMicrotask = function(callback) { | |
| Promise.resolve().then(callback); | |
| }; | |
| } | |
| // The callback and promise forms of fs, over the native synchronous calls. Deferring to a | |
| // microtask is what makes them asynchronous; the work itself still blocks. | |
| // https://nodejs.org/api/fs.html | |
| const fs = globalThis._fs_polyfill; | |
| fs.readFile = function(path, options, callback) { | |
| if (typeof options === 'function') { | |
| callback = options; | |
| options = undefined; | |
| } | |
| Promise.resolve().then(() => { | |
| let data; | |
| try { | |
| data = fs.readFileSync(path, options); | |
| } catch (err) { | |
| callback(err); | |
| return; | |
| } | |
| callback(null, data); | |
| }); | |
| }; | |
| fs.promises = { | |
| readdir: (path) => Promise.resolve().then(() => fs.readdirSync(path)), | |
| stat: (path) => Promise.resolve().then(() => fs.statSync(path)), | |
| readFile: (path, options) => Promise.resolve().then(() => fs.readFileSync(path, options)), | |
| }; | |
| })(); | |
| )bootstrap"; | |
| // Runs the JavaScript half of the polyfills, for the globals that are simpler to express in | |
| // script than to assemble through the C++ API. | |
| void RunBootstrapScript(Napi::Env env) { | |
| napi_value script_src; | |
| napi_create_string_utf8(env, kBootstrapScript, NAPI_AUTO_LENGTH, &script_src); | |
| napi_value result; | |
| napi_run_script(env, script_src, &result); | |
| } | |
| } // namespace | |
| void RegisterPolyfills(Napi::Env env, EventLoop& loop, const PolyfillOptions& options) { | |
| auto* ctx = new PolyfillContext(loop, options); | |
| RegisterConsole(env); | |
| RegisterFs(env); | |
| RegisterPath(env); | |
| RegisterProcess(env, options, ctx); | |
| RegisterTimers(env, ctx); | |
| RegisterPerformance(env, ctx); | |
| RegisterTextEncoder(env); | |
| RegisterRequire(env, ctx); | |
| RunBootstrapScript(env); | |
| } | |
| } // namespace dawn::node::standalone |