#include #include // PATH_MAX #include // S_IFDIR #include "module_wrap.h" #include "env.h" #include "node_url.h" #include "util-inl.h" #include "node_internals.h" namespace node { namespace loader { using node::url::URL; using node::url::URL_FLAGS_FAILED; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; using v8::Integer; using v8::IntegrityLevel; using v8::Isolate; using v8::JSON; using v8::Just; using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Module; using v8::Nothing; using v8::Object; using v8::Promise; using v8::ScriptCompiler; using v8::ScriptOrigin; using v8::String; using v8::TryCatch; using v8::Value; static const char* const EXTENSIONS[] = {".mjs", ".js", ".json", ".node"}; ModuleWrap::ModuleWrap(Environment* env, Local object, Local module, Local url) : BaseObject(env, object) { module_.Reset(env->isolate(), module); url_.Reset(env->isolate(), url); } ModuleWrap::~ModuleWrap() { HandleScope scope(env()->isolate()); Local module = module_.Get(env()->isolate()); auto range = env()->module_map.equal_range(module->GetIdentityHash()); for (auto it = range.first; it != range.second; ++it) { if (it->second == this) { env()->module_map.erase(it); break; } } module_.Reset(); } void ModuleWrap::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); if (!args.IsConstructCall()) { env->ThrowError("constructor must be called using new"); return; } if (args.Length() != 2) { env->ThrowError("constructor must have exactly 2 arguments " "(string, string)"); return; } if (!args[0]->IsString()) { env->ThrowError("first argument is not a string"); return; } Local source_text = args[0].As(); if (!args[1]->IsString()) { env->ThrowError("second argument is not a string"); return; } Local url = args[1].As(); Local module; // compile { ScriptOrigin origin(url, Integer::New(isolate, 0), // line offset Integer::New(isolate, 0), // column offset False(isolate), // is cross origin Local(), // script id Local(), // source map URL False(isolate), // is opaque (?) False(isolate), // is WASM True(isolate)); // is ES6 module TryCatch try_catch(isolate); ScriptCompiler::Source source(source_text, origin); if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { CHECK(try_catch.HasCaught()); CHECK(!try_catch.Message().IsEmpty()); CHECK(!try_catch.Exception().IsEmpty()); AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(), ErrorHandlingMode::MODULE_ERROR); try_catch.ReThrow(); return; } } Local that = args.This(); Local context = that->CreationContext(); Local url_str = FIXED_ONE_BYTE_STRING(isolate, "url"); if (!that->Set(context, url_str, url).FromMaybe(false)) { return; } ModuleWrap* obj = new ModuleWrap(env, that, module, url); env->module_map.emplace(module->GetIdentityHash(), obj); Wrap(that, obj); that->SetIntegrityLevel(context, IntegrityLevel::kFrozen); args.GetReturnValue().Set(that); } void ModuleWrap::Link(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); if (!args[0]->IsFunction()) { env->ThrowError("first argument is not a function"); return; } Local resolver_arg = args[0].As(); Local that = args.This(); ModuleWrap* obj = Unwrap(that); CHECK_NE(obj, nullptr); Local mod_context = that->CreationContext(); if (obj->linked_) return; obj->linked_ = true; Local module(obj->module_.Get(isolate)); // call the dependency resolve callbacks for (int i = 0; i < module->GetModuleRequestsLength(); i++) { Local specifier = module->GetModuleRequest(i); Utf8Value specifier_utf8(env->isolate(), specifier); std::string specifier_std(*specifier_utf8, specifier_utf8.length()); Local argv[] = { specifier }; MaybeLocal maybe_resolve_return_value = resolver_arg->Call(mod_context, that, 1, argv); if (maybe_resolve_return_value.IsEmpty()) { return; } Local resolve_return_value = maybe_resolve_return_value.ToLocalChecked(); if (!resolve_return_value->IsPromise()) { env->ThrowError("linking error, expected resolver to return a promise"); } Local resolve_promise = resolve_return_value.As(); obj->resolve_cache_[specifier_std].Reset(env->isolate(), resolve_promise); } args.GetReturnValue().Set(that); } void ModuleWrap::Instantiate(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); Local that = args.This(); Local context = that->CreationContext(); ModuleWrap* obj = Unwrap(that); CHECK_NE(obj, nullptr); Local module = obj->module_.Get(isolate); TryCatch try_catch(isolate); Maybe ok = module->InstantiateModule(context, ModuleWrap::ResolveCallback); // clear resolve cache on instantiate for (auto& entry : obj->resolve_cache_) entry.second.Reset(); obj->resolve_cache_.clear(); if (!ok.FromMaybe(false)) { CHECK(try_catch.HasCaught()); CHECK(!try_catch.Message().IsEmpty()); CHECK(!try_catch.Exception().IsEmpty()); AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(), ErrorHandlingMode::MODULE_ERROR); try_catch.ReThrow(); return; } } void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); Local that = args.This(); Local context = that->CreationContext(); ModuleWrap* obj = Unwrap(that); CHECK_NE(obj, nullptr); MaybeLocal result = obj->module_.Get(isolate)->Evaluate(context); if (result.IsEmpty()) { return; } args.GetReturnValue().Set(result.ToLocalChecked()); } void ModuleWrap::Namespace(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); Local that = args.This(); ModuleWrap* obj = Unwrap(that); CHECK_NE(obj, nullptr); Local module = obj->module_.Get(isolate); switch (module->GetStatus()) { default: return env->ThrowError( "cannot get namespace, Module has not been instantiated"); case v8::Module::Status::kInstantiated: case v8::Module::Status::kEvaluating: case v8::Module::Status::kEvaluated: break; } Local result = module->GetModuleNamespace(); args.GetReturnValue().Set(result); } MaybeLocal ModuleWrap::ResolveCallback(Local context, Local specifier, Local referrer) { Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); if (env->module_map.count(referrer->GetIdentityHash()) == 0) { env->ThrowError("linking error, unknown module"); return MaybeLocal(); } ModuleWrap* dependent = nullptr; auto range = env->module_map.equal_range(referrer->GetIdentityHash()); for (auto it = range.first; it != range.second; ++it) { if (it->second->module_ == referrer) { dependent = it->second; break; } } if (dependent == nullptr) { env->ThrowError("linking error, null dep"); return MaybeLocal(); } Utf8Value specifier_utf8(env->isolate(), specifier); std::string specifier_std(*specifier_utf8, specifier_utf8.length()); if (dependent->resolve_cache_.count(specifier_std) != 1) { env->ThrowError("linking error, not in local cache"); return MaybeLocal(); } Local resolve_promise = dependent->resolve_cache_[specifier_std].Get(isolate); if (resolve_promise->State() != Promise::kFulfilled) { env->ThrowError("linking error, dependency promises must be resolved on " "instantiate"); return MaybeLocal(); } Local module_object = resolve_promise->Result().As(); if (module_object.IsEmpty() || !module_object->IsObject()) { env->ThrowError("linking error, expected a valid module object from " "resolver"); return MaybeLocal(); } ModuleWrap* module; ASSIGN_OR_RETURN_UNWRAP(&module, module_object, MaybeLocal()); return module->module_.Get(env->isolate()); } namespace { // Tests whether a path starts with /, ./ or ../ // In WhatWG terminology, the alternative case is called a "bare" specifier // (e.g. in `import "jquery"`). inline bool ShouldBeTreatedAsRelativeOrAbsolutePath( const std::string& specifier) { size_t len = specifier.length(); if (len == 0) return false; if (specifier[0] == '/') { return true; } else if (specifier[0] == '.') { if (len == 1 || specifier[1] == '/') return true; if (specifier[1] == '.') { if (len == 2 || specifier[2] == '/') return true; } } return false; } std::string ReadFile(uv_file file) { std::string contents; uv_fs_t req; char buffer_memory[4096]; uv_buf_t buf = uv_buf_init(buffer_memory, sizeof(buffer_memory)); int r; do { r = uv_fs_read(uv_default_loop(), &req, file, &buf, 1, contents.length(), // offset nullptr); uv_fs_req_cleanup(&req); if (r <= 0) break; contents.append(buf.base, r); } while (true); return contents; } enum CheckFileOptions { LEAVE_OPEN_AFTER_CHECK, CLOSE_AFTER_CHECK }; Maybe CheckFile(const URL& search, CheckFileOptions opt = CLOSE_AFTER_CHECK) { uv_fs_t fs_req; std::string path = search.ToFilePath(); if (path.empty()) { return Nothing(); } uv_file fd = uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr); uv_fs_req_cleanup(&fs_req); if (fd < 0) { return Nothing(); } uv_fs_fstat(nullptr, &fs_req, fd, nullptr); uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; uv_fs_req_cleanup(&fs_req); if (is_directory) { uv_fs_close(nullptr, &fs_req, fd, nullptr); uv_fs_req_cleanup(&fs_req); return Nothing(); } if (opt == CLOSE_AFTER_CHECK) { uv_fs_close(nullptr, &fs_req, fd, nullptr); uv_fs_req_cleanup(&fs_req); } return Just(fd); } enum ResolveExtensionsOptions { TRY_EXACT_NAME, ONLY_VIA_EXTENSIONS }; template Maybe ResolveExtensions(const URL& search) { if (options == TRY_EXACT_NAME) { Maybe check = CheckFile(search); if (!check.IsNothing()) { return Just(search); } } for (const char* extension : EXTENSIONS) { URL guess(search.path() + extension, &search); Maybe check = CheckFile(guess); if (!check.IsNothing()) { return Just(guess); } } return Nothing(); } inline Maybe ResolveIndex(const URL& search) { return ResolveExtensions(URL("index", search)); } Maybe ResolveMain(Environment* env, const URL& search) { URL pkg("package.json", &search); Maybe check = CheckFile(pkg, LEAVE_OPEN_AFTER_CHECK); if (check.IsNothing()) { return Nothing(); } Isolate* isolate = env->isolate(); Local context = isolate->GetCurrentContext(); std::string pkg_src = ReadFile(check.FromJust()); uv_fs_t fs_req; uv_fs_close(nullptr, &fs_req, check.FromJust(), nullptr); uv_fs_req_cleanup(&fs_req); // It's not okay for the called of this method to not be able to tell // whether an exception is pending or not. TryCatch try_catch(isolate); Local src; if (!String::NewFromUtf8(isolate, pkg_src.c_str(), v8::NewStringType::kNormal, pkg_src.length()).ToLocal(&src)) { return Nothing(); } Local pkg_json; if (!JSON::Parse(context, src).ToLocal(&pkg_json) || !pkg_json->IsObject()) return Nothing(); Local pkg_main; if (!pkg_json.As()->Get(context, env->main_string()) .ToLocal(&pkg_main) || !pkg_main->IsString()) { return Nothing(); } Utf8Value main_utf8(isolate, pkg_main.As()); std::string main_std(*main_utf8, main_utf8.length()); if (!ShouldBeTreatedAsRelativeOrAbsolutePath(main_std)) { main_std.insert(0, "./"); } return Resolve(env, main_std, search); } Maybe ResolveModule(Environment* env, const std::string& specifier, const URL& base) { URL parent(".", base); URL dir(""); do { dir = parent; Maybe check = Resolve(env, "./node_modules/" + specifier, dir, true); if (!check.IsNothing()) { const size_t limit = specifier.find('/'); const size_t spec_len = limit == std::string::npos ? specifier.length() : limit + 1; std::string chroot = dir.path() + "node_modules/" + specifier.substr(0, spec_len); if (check.FromJust().path().substr(0, chroot.length()) != chroot) { return Nothing(); } return check; } else { // TODO(bmeck) PREVENT FALLTHROUGH } parent = URL("..", &dir); } while (parent.path() != dir.path()); return Nothing(); } Maybe ResolveDirectory(Environment* env, const URL& search, bool read_pkg_json) { if (read_pkg_json) { Maybe main = ResolveMain(env, search); if (!main.IsNothing()) return main; } return ResolveIndex(search); } } // anonymous namespace Maybe Resolve(Environment* env, const std::string& specifier, const URL& base, bool read_pkg_json) { URL pure_url(specifier); if (!(pure_url.flags() & URL_FLAGS_FAILED)) { // just check existence, without altering Maybe check = CheckFile(pure_url); if (check.IsNothing()) { return Nothing(); } return Just(pure_url); } if (specifier.length() == 0) { return Nothing(); } if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { URL resolved(specifier, base); Maybe file = ResolveExtensions(resolved); if (!file.IsNothing()) return file; if (specifier.back() != '/') { resolved = URL(specifier + "/", base); } return ResolveDirectory(env, resolved, read_pkg_json); } else { return ResolveModule(env, specifier, base); } } void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); if (args.IsConstructCall()) { env->ThrowError("resolve() must not be called as a constructor"); return; } if (args.Length() != 2) { env->ThrowError("resolve must have exactly 2 arguments (string, string)"); return; } if (!args[0]->IsString()) { env->ThrowError("first argument is not a string"); return; } Utf8Value specifier_utf8(env->isolate(), args[0]); std::string specifier_std(*specifier_utf8, specifier_utf8.length()); if (!args[1]->IsString()) { env->ThrowError("second argument is not a string"); return; } Utf8Value url_utf8(env->isolate(), args[1]); URL url(*url_utf8, url_utf8.length()); if (url.flags() & URL_FLAGS_FAILED) { env->ThrowError("second argument is not a URL string"); return; } Maybe result = node::loader::Resolve(env, specifier_std, url, true); if (result.IsNothing() || (result.FromJust().flags() & URL_FLAGS_FAILED)) { std::string msg = "Cannot find module " + specifier_std; env->ThrowError(msg.c_str()); return; } args.GetReturnValue().Set(result.FromJust().ToObject(env)); } static MaybeLocal ImportModuleDynamically( Local context, Local referrer, Local specifier) { Isolate* iso = context->GetIsolate(); Environment* env = Environment::GetCurrent(context); v8::EscapableHandleScope handle_scope(iso); if (env->context() != context) { auto maybe_resolver = Promise::Resolver::New(context); Local resolver; if (maybe_resolver.ToLocal(&resolver)) { // TODO(jkrems): Turn into proper error object w/ code Local error = v8::Exception::Error( OneByteString(iso, "import() called outside of main context")); if (resolver->Reject(context, error).IsJust()) { return handle_scope.Escape(resolver.As()); } } return MaybeLocal(); } Local import_callback = env->host_import_module_dynamically_callback(); Local import_args[] = { referrer->GetResourceName(), Local(specifier) }; MaybeLocal maybe_result = import_callback->Call(context, v8::Undefined(iso), 2, import_args); Local result; if (maybe_result.ToLocal(&result)) { return handle_scope.Escape(result.As()); } return MaybeLocal(); } void ModuleWrap::SetImportModuleDynamicallyCallback( const FunctionCallbackInfo& args) { Isolate* iso = args.GetIsolate(); Environment* env = Environment::GetCurrent(args); HandleScope handle_scope(iso); if (!args[0]->IsFunction()) { env->ThrowError("first argument is not a function"); return; } Local import_callback = args[0].As(); env->set_host_import_module_dynamically_callback(import_callback); iso->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically); } void ModuleWrap::Initialize(Local target, Local unused, Local context) { Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); Local tpl = env->NewFunctionTemplate(New); tpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap")); tpl->InstanceTemplate()->SetInternalFieldCount(1); env->SetProtoMethod(tpl, "link", Link); env->SetProtoMethod(tpl, "instantiate", Instantiate); env->SetProtoMethod(tpl, "evaluate", Evaluate); env->SetProtoMethod(tpl, "namespace", Namespace); target->Set(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction()); env->SetMethod(target, "resolve", node::loader::ModuleWrap::Resolve); env->SetMethod(target, "setImportModuleDynamicallyCallback", node::loader::ModuleWrap::SetImportModuleDynamicallyCallback); } } // namespace loader } // namespace node NODE_MODULE_CONTEXT_AWARE_INTERNAL(module_wrap, node::loader::ModuleWrap::Initialize)