blob: 8553f9fa40bca576d8ca3c575f2ab86f653da8c3 [file] [edit]
// 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