Repository: nodejs/node
Pull request: src: port `initializeImportMeta` to native

Supersedes https://github.com/nodejs/node/pull/57003.

We should probably avoid translating the URL to a path twice when the user needs both `dirname` and `filename`, but I'm not sure if there's an elegant way to do it without crossing the C++/JS bundary.
<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/modules/esm/initialize_import_meta.js
patch: @@ -5,8 +5,10 @@ const {
 } = primordials;
 
 const { getOptionValue } = require('internal/options');
-const { fileURLToPath } = require('internal/url');
-const { dirname } = require('path');
+const {
+  setLazyPathHelpers,
+} = internalBinding('modules');
+
 const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');
 
 /**
@@ -58,11 +60,9 @@ function initializeImportMeta(meta, context, loader) {
 
   // Alphabetical
   if (StringPrototypeStartsWith(url, 'file:') === true) {
-    // These only make sense for locally loaded modules,
-    // i.e. network modules are not supported.
-    const filePath = fileURLToPath(url);
-    meta.dirname = dirname(filePath);
-    meta.filename = filePath;
+    // dirname
+    // filename
+    setLazyPathHelpers(meta, url);
   }
 
   if (!loader || loader.allowImportMetaResolve) {

filename: src/env_properties.h
patch: @@ -125,6 +125,7 @@
   V(destroyed_string, "destroyed")                                             \
   V(detached_string, "detached")                                               \
   V(dh_string, "DH")                                                           \
+  V(dirname_string, "dirname")                                                 \
   V(divisor_length_string, "divisorLength")                                    \
   V(dns_a_string, "A")                                                         \
   V(dns_aaaa_string, "AAAA")                                                   \
@@ -335,6 +336,7 @@
     "export * from 'original'; export { default } from 'original'; export "    \
     "const __esModule = true;")                                                \
   V(require_string, "require")                                                 \
+  V(resolve_string, "resolve")                                                 \
   V(resource_string, "resource")                                               \
   V(result_string, "result")                                                   \
   V(retry_string, "retry")                                                     \

filename: src/node_modules.cc
patch: @@ -35,6 +35,7 @@ using v8::Null;
 using v8::Object;
 using v8::ObjectTemplate;
 using v8::Primitive;
+using v8::PropertyCallbackInfo;
 using v8::String;
 using v8::Undefined;
 using v8::Value;
@@ -594,6 +595,76 @@ void GetCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
       isolate, v8::Null(isolate), names.data(), values.data(), names.size()));
 }
 
+static void PathHelpersLazyGetter(Local<v8::Name> name,
+                                  const PropertyCallbackInfo<Value>& info) {
+  Isolate* isolate = info.GetIsolate();
+  // This getter has no JavaScript function representation and is not
+  // invoked in the creation context.
+  // When this getter is invoked in a vm context, the `Realm::GetCurrent(info)`
+  // returns a nullptr and retrieve the creation context via `this` object and
+  // get the creation Realm.
+  Local<Value> receiver_val = info.This();
+  if (!receiver_val->IsObject()) {
+    THROW_ERR_INVALID_INVOCATION(isolate);
+    return;
+  }
+  Local<Object> receiver = receiver_val.As<Object>();
+  Local<Context> context;
+  if (!receiver->GetCreationContext().ToLocal(&context)) {
+    THROW_ERR_INVALID_INVOCATION(isolate);
+    return;
+  }
+  Environment* env = Environment::GetCurrent(context);
+
+  node::Utf8Value url(isolate, info.Data());
+  auto file_url = ada::parse(url.ToStringView());
+  CHECK(file_url);
+  auto file_path = url::FileURLToPath(env, *file_url);
+  CHECK(file_path.has_value());
+  std::string_view ret_view = file_path.value();
+
+  node::Utf8Value utf8name(isolate, name);
+  auto plain_name = utf8name.ToStringView();
+  if (plain_name == "dirname") {
+#ifdef _WIN32
+#define PATH_SEPARATOR '\\'
+#else
+#define PATH_SEPARATOR '/'
+#endif
+    auto index = ret_view.rfind(PATH_SEPARATOR);
+    CHECK(index != std::string_view::npos);
+    ret_view.remove_suffix(ret_view.size() - index);
+#undef PATH_SEPARATOR
+  }
+  Local<Value> ret;
+  if (!ToV8Value(context, ret_view, isolate).ToLocal(&ret)) {
+    return;
+  }
+  info.GetReturnValue().Set(ret);
+}
+void InitImportMetaPathHelpers(const FunctionCallbackInfo<Value>& args) {
+  // target, url, shouldSetDirnameAndFilename, resolve
+  CHECK_GE(args.Length(), 2);
+  CHECK(args[0]->IsObject());
+  CHECK(args[1]->IsString());
+
+  Isolate* isolate = args.GetIsolate();
+  Local<Context> context = isolate->GetCurrentContext();
+  Environment* env = Environment::GetCurrent(context);
+
+  auto target = args[0].As<Object>();
+
+  // N.B.: Order is important to keep keys in alphabetical order.
+  if (target
+          ->SetLazyDataProperty(
+              context, env->dirname_string(), PathHelpersLazyGetter, args[1])
+          .IsNothing() ||
+      target
+          ->SetLazyDataProperty(
+              context, env->filename_string(), PathHelpersLazyGetter, args[1])
+          .IsNothing())
+    return;
+}
 void SaveCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
   Isolate* isolate = args.GetIsolate();
   Local<Context> context = isolate->GetCurrentContext();
@@ -627,6 +698,7 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
   SetMethod(isolate, target, "flushCompileCache", FlushCompileCache);
   SetMethod(isolate, target, "getCompileCacheEntry", GetCompileCacheEntry);
   SetMethod(isolate, target, "saveCompileCacheEntry", SaveCompileCacheEntry);
+  SetMethod(isolate, target, "setLazyPathHelpers", InitImportMetaPathHelpers);
 }
 
 void BindingData::CreatePerContextProperties(Local<Object> target,
@@ -682,6 +754,7 @@ void BindingData::RegisterExternalReferences(
   registry->Register(FlushCompileCache);
   registry->Register(GetCompileCacheEntry);
   registry->Register(SaveCompileCacheEntry);
+  registry->Register(InitImportMetaPathHelpers);
 }
 
 }  // namespace modules