Repository: electron/electron
Pull request: fix: ESM-from-CJS import when CJK is in path

#### Description of Change

Backport of #48855

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [ ] `npm test` passes
- [ ] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [ ] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: fix: ESM-from-CJS import when CJK characters are in path

cc @deepak1556 @codebytere 


Changed files:
filename: patches/node/.patches
patch: @@ -52,3 +52,4 @@ api_remove_deprecated_getisolate.patch
 src_switch_from_get_setprototype_to_get_setprototypev2.patch
 fix_replace_deprecated_setprototype.patch
 fix_redefined_macos_sdk_header_symbols.patch
+src_use_cp_utf8_for_wide_file_names_on_win32.patch

filename: patches/node/src_use_cp_utf8_for_wide_file_names_on_win32.patch
patch: @@ -0,0 +1,312 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Fedor Indutny <238531+indutny@users.noreply.github.com>
+Date: Fri, 7 Nov 2025 19:41:44 -0800
+Subject: src: use CP_UTF8 for wide file names on win32
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+`src/node_modules.cc` needs to be consistent with `src/node_file.cc` in
+how it translates the utf8 strings to `std::wstring` otherwise we might
+end up in situation where we can read the source code of imported
+package from disk, but fail to recognize that it is an ESM (or CJS) and
+cause runtime errors. This type of error is possible on Windows when the
+path contains unicode characters and "Language for non-Unicode programs"
+is set to "Chinese (Traditional, Taiwan)".
+
+See: #58768
+PR-URL: https://github.com/nodejs/node/pull/60575
+Reviewed-By: Anna Henningsen <anna@addaleax.net>
+Reviewed-By: Darshan Sen <raisinten@gmail.com>
+Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com>
+Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com>
+Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
+Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
+
+diff --git a/src/node_file.cc b/src/node_file.cc
+index 75be21c9e8b413f522240a906da06d26c44d5b71..e94c2b5f2cf7cac413cd5cb782fa1cca6d764960 100644
+--- a/src/node_file.cc
++++ b/src/node_file.cc
+@@ -3056,42 +3056,6 @@ static void GetFormatOfExtensionlessFile(
+   return args.GetReturnValue().Set(EXTENSIONLESS_FORMAT_JAVASCRIPT);
+ }
+ 
+-#ifdef _WIN32
+-#define BufferValueToPath(str)                                                 \
+-  std::filesystem::path(ConvertToWideString(str.ToString(), CP_UTF8))
+-
+-std::string ConvertWideToUTF8(const std::wstring& wstr) {
+-  if (wstr.empty()) return std::string();
+-
+-  int size_needed = WideCharToMultiByte(CP_UTF8,
+-                                        0,
+-                                        &wstr[0],
+-                                        static_cast<int>(wstr.size()),
+-                                        nullptr,
+-                                        0,
+-                                        nullptr,
+-                                        nullptr);
+-  std::string strTo(size_needed, 0);
+-  WideCharToMultiByte(CP_UTF8,
+-                      0,
+-                      &wstr[0],
+-                      static_cast<int>(wstr.size()),
+-                      &strTo[0],
+-                      size_needed,
+-                      nullptr,
+-                      nullptr);
+-  return strTo;
+-}
+-
+-#define PathToString(path) ConvertWideToUTF8(path.wstring());
+-
+-#else  // _WIN32
+-
+-#define BufferValueToPath(str) std::filesystem::path(str.ToStringView());
+-#define PathToString(path) path.native();
+-
+-#endif  // _WIN32
+-
+ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
+   Environment* env = Environment::GetCurrent(args);
+   Isolate* isolate = env->isolate();
+@@ -3104,7 +3068,7 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
+   THROW_IF_INSUFFICIENT_PERMISSIONS(
+       env, permission::PermissionScope::kFileSystemRead, src.ToStringView());
+ 
+-  auto src_path = BufferValueToPath(src);
++  auto src_path = src.ToPath();
+ 
+   BufferValue dest(isolate, args[1]);
+   CHECK_NOT_NULL(*dest);
+@@ -3112,7 +3076,7 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
+   THROW_IF_INSUFFICIENT_PERMISSIONS(
+       env, permission::PermissionScope::kFileSystemWrite, dest.ToStringView());
+ 
+-  auto dest_path = BufferValueToPath(dest);
++  auto dest_path = dest.ToPath();
+   bool dereference = args[2]->IsTrue();
+   bool recursive = args[3]->IsTrue();
+ 
+@@ -3141,8 +3105,8 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
+       (src_status.type() == std::filesystem::file_type::directory) ||
+       (dereference && src_status.type() == std::filesystem::file_type::symlink);
+ 
+-  auto src_path_str = PathToString(src_path);
+-  auto dest_path_str = PathToString(dest_path);
++  auto src_path_str = ConvertPathToUTF8(src_path);
++  auto dest_path_str = ConvertPathToUTF8(dest_path);
+ 
+   if (!error_code) {
+     // Check if src and dest are identical.
+@@ -3237,7 +3201,7 @@ static bool CopyUtimes(const std::filesystem::path& src,
+   uv_fs_t req;
+   auto cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
+ 
+-  auto src_path_str = PathToString(src);
++  auto src_path_str = ConvertPathToUTF8(src);
+   int result = uv_fs_stat(nullptr, &req, src_path_str.c_str(), nullptr);
+   if (is_uv_error(result)) {
+     env->ThrowUVException(result, "stat", nullptr, src_path_str.c_str());
+@@ -3248,7 +3212,7 @@ static bool CopyUtimes(const std::filesystem::path& src,
+   const double source_atime = s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9;
+   const double source_mtime = s->st_mtim.tv_sec + s->st_mtim.tv_nsec / 1e9;
+ 
+-  auto dest_file_path_str = PathToString(dest);
++  auto dest_file_path_str = ConvertPathToUTF8(dest);
+   int utime_result = uv_fs_utime(nullptr,
+                                  &req,
+                                  dest_file_path_str.c_str(),
+@@ -3383,7 +3347,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
+     std::error_code error;
+     for (auto dir_entry : std::filesystem::directory_iterator(src)) {
+       auto dest_file_path = dest / dir_entry.path().filename();
+-      auto dest_str = PathToString(dest);
++      auto dest_str = ConvertPathToUTF8(dest);
+ 
+       if (dir_entry.is_symlink()) {
+         if (verbatim_symlinks) {
+@@ -3446,7 +3410,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
+               }
+             } else if (std::filesystem::is_regular_file(dest_file_path)) {
+               if (!dereference || (!force && error_on_exist)) {
+-                auto dest_file_path_str = PathToString(dest_file_path);
++                auto dest_file_path_str = ConvertPathToUTF8(dest_file_path);
+                 env->ThrowStdErrException(
+                     std::make_error_code(std::errc::file_exists),
+                     "cp",
+diff --git a/src/node_modules.cc b/src/node_modules.cc
+index 14f2a35f87e8c2fa17898147d7247cc00c066f35..871282c6f8780ee0bca1e7230c0c2d83fd0c98c0 100644
+--- a/src/node_modules.cc
++++ b/src/node_modules.cc
+@@ -360,12 +360,13 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
+ 
+     // Stop the search when the process doesn't have permissions
+     // to walk upwards
+-    if (is_permissions_enabled &&
+-        !env->permission()->is_granted(
+-            env,
+-            permission::PermissionScope::kFileSystemRead,
+-            current_path.generic_string())) [[unlikely]] {
+-      return nullptr;
++    if (is_permissions_enabled) {
++      if (!env->permission()->is_granted(
++              env,
++              permission::PermissionScope::kFileSystemRead,
++              ConvertGenericPathToUTF8(current_path))) [[unlikely]] {
++        return nullptr;
++      }
+     }
+ 
+     // If current path is outside the resources path, bail.
+@@ -375,13 +376,14 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
+     }
+ 
+     // Check if the path ends with `/node_modules`
+-    if (current_path.generic_string().ends_with("/node_modules")) {
++    if (current_path.filename() == "node_modules") {
+       return nullptr;
+     }
+ 
+     auto package_json_path = current_path / "package.json";
++
+     auto package_json =
+-        GetPackageJSON(realm, package_json_path.string(), nullptr);
++        GetPackageJSON(realm, ConvertPathToUTF8(package_json_path), nullptr);
+     if (package_json != nullptr) {
+       return package_json;
+     }
+@@ -403,20 +405,12 @@ void BindingData::GetNearestParentPackageJSONType(
+ 
+   ToNamespacedPath(realm->env(), &path_value);
+ 
+-  std::string path_value_str = path_value.ToString();
++  auto path = path_value.ToPath();
++
+   if (slashCheck) {
+-    path_value_str.push_back(kPathSeparator);
++    path /= "";
+   }
+ 
+-  std::filesystem::path path;
+-
+-#ifdef _WIN32
+-  std::wstring wide_path = ConvertToWideString(path_value_str, GetACP());
+-  path = std::filesystem::path(wide_path);
+-#else
+-  path = std::filesystem::path(path_value_str);
+-#endif
+-
+   auto package_json = TraverseParent(realm, path);
+ 
+   if (package_json == nullptr) {
+diff --git a/src/util-inl.h b/src/util-inl.h
+index 816156282790383e896b28eb46a3b4703bbe17f0..7274e5da8cc9eb164ec46ec2f7932691ed6ba9dc 100644
+--- a/src/util-inl.h
++++ b/src/util-inl.h
+@@ -678,12 +678,11 @@ inline bool IsWindowsBatchFile(const char* filename) {
+   return !extension.empty() && (extension == "cmd" || extension == "bat");
+ }
+ 
+-inline std::wstring ConvertToWideString(const std::string& str,
+-                                        UINT code_page) {
++inline std::wstring ConvertUTF8ToWideString(const std::string& str) {
+   int size_needed = MultiByteToWideChar(
+-      code_page, 0, &str[0], static_cast<int>(str.size()), nullptr, 0);
++      CP_UTF8, 0, &str[0], static_cast<int>(str.size()), nullptr, 0);
+   std::wstring wstrTo(size_needed, 0);
+-  MultiByteToWideChar(code_page,
++  MultiByteToWideChar(CP_UTF8,
+                       0,
+                       &str[0],
+                       static_cast<int>(str.size()),
+@@ -691,6 +690,59 @@ inline std::wstring ConvertToWideString(const std::string& str,
+                       size_needed);
+   return wstrTo;
+ }
++
++std::string ConvertWideStringToUTF8(const std::wstring& wstr) {
++  if (wstr.empty()) return std::string();
++
++  int size_needed = WideCharToMultiByte(CP_UTF8,
++                                        0,
++                                        &wstr[0],
++                                        static_cast<int>(wstr.size()),
++                                        nullptr,
++                                        0,
++                                        nullptr,
++                                        nullptr);
++  std::string strTo(size_needed, 0);
++  WideCharToMultiByte(CP_UTF8,
++                      0,
++                      &wstr[0],
++                      static_cast<int>(wstr.size()),
++                      &strTo[0],
++                      size_needed,
++                      nullptr,
++                      nullptr);
++  return strTo;
++}
++
++template <typename T, size_t kStackStorageSize>
++std::filesystem::path MaybeStackBuffer<T, kStackStorageSize>::ToPath() const {
++  std::wstring wide_path = ConvertUTF8ToWideString(ToString());
++  return std::filesystem::path(wide_path);
++}
++
++std::string ConvertPathToUTF8(const std::filesystem::path& path) {
++  return ConvertWideStringToUTF8(path.wstring());
++}
++
++std::string ConvertGenericPathToUTF8(const std::filesystem::path& path) {
++  return ConvertWideStringToUTF8(path.generic_wstring());
++}
++
++#else  // _WIN32
++
++template <typename T, size_t kStackStorageSize>
++std::filesystem::path MaybeStackBuffer<T, kStackStorageSize>::ToPath() const {
++  return std::filesystem::path(ToStringView());
++}
++
++std::string ConvertPathToUTF8(const std::filesystem::path& path) {
++  return path.native();
++}
++
++std::string ConvertGenericPathToUTF8(const std::filesystem::path& path) {
++  return path.generic_string();
++}
++
+ #endif  // _WIN32
+ 
+ }  // namespace node
+diff --git a/src/util.h b/src/util.h
+index dab48c59e1cd947a32cf08e5ab23cd60fe32303e..91bfb8d94b1c053b59c20e25306ef0f08e977f49 100644
+--- a/src/util.h
++++ b/src/util.h
+@@ -507,6 +507,8 @@ class MaybeStackBuffer {
+   inline std::basic_string_view<T> ToStringView() const {
+     return {out(), length()};
+   }
++  // This can only be used if the buffer contains path data in UTF8
++  inline std::filesystem::path ToPath() const;
+ 
+  private:
+   size_t length_;
+@@ -1022,9 +1024,15 @@ class JSONOutputStream final : public v8::OutputStream {
+ // Returns true if OS==Windows and filename ends in .bat or .cmd,
+ // case insensitive.
+ inline bool IsWindowsBatchFile(const char* filename);
+-inline std::wstring ConvertToWideString(const std::string& str, UINT code_page);
++inline std::wstring ConvertUTF8ToWideString(const std::string& str);
++inline std::string ConvertWideStringToUTF8(const std::wstring& wstr);
++
+ #endif  // _WIN32
+ 
++inline std::filesystem::path ConvertUTF8ToPath(const std::string& str);
++inline std::string ConvertPathToUTF8(const std::filesystem::path& path);
++inline std::string ConvertGenericPathToUTF8(const std::filesystem::path& path);
++
+ }  // namespace node
+ 
+ #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS