Repository: electron/electron (language: cpp)

Issue:
[Bug]: Electron 28 breaks `import` of transitive dependencies inside ES modules
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a bug report that matches the one I want to file, without success.

### Electron Version

28.0.0-alpha.2

### What operating system are you using?

Windows

### Operating System Version

Windows 10

### What arch are you using?

x64

### Last Known Working Electron version

27.0.0

### Expected Behavior

Importing an ESM dependency which imports some other dependency (ESM or CJS) should work from inside `app.asar`

### Actual Behavior

The module lookup starts at ```node_modules/<directory where the dependency's `main` file lives>/node_modules```, doesn't find the transitive dependency there and then crashes, never reaching the top-level `node_modules` which is where the transitive dependency actually lives:

![image](https://github.com/electron/electron/assets/19478575/e1febb84-d9de-447c-9757-6439add5fa7a)


### Testcase Gist URL

_No response_

### Additional Information

I've run into this using [this repro](https://github.com/electron/asar/issues/249#issuecomment-1434998313), where the ESM dependency `got` is imported causing the app to crash when looking for the transitive ESM dependency `@sindresorhus/is`. I've put together a minimal reproduction with two local dependencies, which should make it easier to test different variables: https://github.com/erikian/esm-asar-demo

Note that this problem seems to be limited to dependencies inside `node_modules`, i.e. ESM imports across project files work fine:
```ts
// src/main.mjs
import printStuff from './printStuff.js'

printStuff('it works!')


// src/printStuff.js
import logger from './logger.js'

export default function printStuff(...stuff) {
  logger(...stuff)
}


// src/logger.js
export default function logger(...args) {
  console.log(...args)
}
``` 

Some findings:
- `const { default: printStuff } = await import('my-esm-dependency')` doesn't work out of the box, but it does work when setting `process.noAsar = true` before importing. Moving the import into the `app.whenReady` callback also doesn't work
- `import printStuff from 'my-esm-dependency'` doesn't work at all, not even with `process.noAsar = true`
- This is not limited to ESM entrypoints. I also get the same errors on the `commonjs-entrypoint` branch, and changing to Electron 27.0.0 fixes the problem.


Discussion / hints:

Fix patch:
diff --git a/lib/asar/fs-wrapper.ts b/lib/asar/fs-wrapper.ts
index e7e92d28f9f1e..eeb6e06239f80 100644
--- a/lib/asar/fs-wrapper.ts
+++ b/lib/asar/fs-wrapper.ts
@@ -261,6 +261,14 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
     fs.writeSync(logFDs.get(asarPath), `${offset}: ${filePath}\n`);
   };
 
+  const shouldThrowStatError = (options: any) => {
+    if (options && typeof options === 'object' && options.throwIfNoEntry === false) {
+      return false;
+    }
+
+    return true;
+  };
+
   const { lstatSync } = fs;
   fs.lstatSync = (pathArgument: string, options: any) => {
     const pathInfo = splitPath(pathArgument);
@@ -268,10 +276,16 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
     const { asarPath, filePath } = pathInfo;
 
     const archive = getOrCreateArchive(asarPath);
-    if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
+    if (!archive) {
+      if (shouldThrowStatError(options)) throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
+      return null;
+    }
 
     const stats = archive.stat(filePath);
-    if (!stats) throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
+    if (!stats) {
+      if (shouldThrowStatError(options)) throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
+      return null;
+    }
 
     return asarStatsToFsStats(stats);
   };
diff --git a/spec/asar-spec.ts b/spec/asar-spec.ts
index ed7a587530843..6caa556cdae20 100644
--- a/spec/asar-spec.ts
+++ b/spec/asar-spec.ts
@@ -490,6 +490,15 @@ describe('asar package', function () {
           }).to.throw(/ENOENT/);
         }
       });
+
+      itremote('returns null when can not find file with throwIfNoEntry === false', function () {
+        const ref2 = ['file4', 'file5', path.join('dir1', 'file4')];
+        for (let j = 0, len = ref2.length; j < len; j++) {
+          const file = ref2[j];
+          const p = path.join(asarDir, 'a.asar', file);
+          expect(fs.lstatSync(p, { throwIfNoEntry: false })).to.equal(null);
+        }
+      });
     });
 
     describe('fs.lstat', function () {


Test patch: