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

#### Description of Change

Upstream fix: https://github.com/nodejs/node/pull/60575

#### 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 @VerteDinde 

Changed files:
filename: patches/node/.patches
patch: @@ -41,3 +41,4 @@ lib_check_sharedarraybuffer_existence_in_fast-utf8-stream.patch
 chore_handle_support_for_import_defer_as_ns_and_import_defer.patch
 api_delete_deprecated_fields_on_v8_isolate.patch
 api_promote_deprecation_of_v8_context_and_v8_object_api_methods.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 969e7d08086f8442bed476feaf15599b8c79db7c..e7459654401c275dfb86207831016ed71060bcc9 100644
+--- a/src/node_file.cc
++++ b/src/node_file.cc
+@@ -3175,42 +3175,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();
+@@ -3223,7 +3187,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);
+@@ -3231,7 +3195,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();
+ 
+@@ -3260,8 +3224,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.
+@@ -3356,7 +3320,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());
+@@ -3367,7 +3331,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(),
+@@ -3502,7 +3466,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) {
+@@ -3565,7 +3529,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 d8477191efafba3c41c06d765f4b03bd00b8573c..5444e556b89ba5b71739753eaafa706cd9727013 100644
+--- a/src/node_modules.cc
++++ b/src/node_modules.cc
+@@ -365,12 +365,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.
+@@ -380,13 +381,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;
+     }
+@@ -408,20 +410,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 da9268dcf2ff432ddeec7c0f61a147b73f3130e2..82b2760f535345d126bc3dcf3890573d36dbb497 100644
+--- a/src/util-inl.h
++++ b/src/util-inl.h
+@@ -698,12 +698,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()),
+@@ -711,6 +710,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
+ 
+ inline v8::MaybeLocal<v8::Object> NewDictionaryInstance(
+diff --git a/src/util.h b/src/util.h
+index 6da57f95165bbdedb65dab6eaae8c39b815ee4e5..e65d6dbf359f61d051efe4e129c9035ed90cc8f6 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_;
+@@ -1026,9 +1028,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);
++
+ // A helper to create a new instance of the dictionary template.
+ // Unlike v8::DictionaryTemplate::NewInstance, this method will
+ // check that all properties have been set (are not empty MaybeLocals)