Repository: electron/electron (language: cpp)

Issue:
[Feature Request]: Support forking ES Modules (.mjs) in electron's utilityProcess
### 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 feature request that matches the one I want to file, without success.

### Problem Description

As of [#21457], the ES Module has now supported in electron-nightly 28.0. This is great, but still I get an error when trying to fork the ES Module in electron-nightly 28.0's UtilityProcess.
Code example:

```
this.process = utilityProcess.fork('<directory_path_to_esm>/name.mjs', args, {
			serviceName, 
			env,
			execArgv,
			allowLoadingUnsignedLibraries,
			forceAllocationsToV8Sandbox,
			stdio
		} as ForkOptions);
```

Errors generated:
```
Error [ERR_REQUIRE_ESM]: require() of ES Module '<directory_path_to_esm>/name.mjs' not supported.
Instead change the require of '<directory_path_to_esm>/name.mjs' to a dynamic import() which is available in all CommonJS modules.
    at f._load (node:electron/js2c/asar_bundle:2:13330)
    at node:electron/js2c/utility_init:2:5946
    at node:electron/js2c/utility_init:2:5961
    at node:electron/js2c/utility_init:2:5965
    at f._load (node:electron/js2c/asar_bundle:2:13330) {
  code: 'ERR_REQUIRE_ESM'
}
```

### Proposed Solution

- Request ability to UtilityProcess of forking the ES Module (.mjs) in electron.

This ability leads to increasing the value of using Electron, so that we can dynamically load arbitrary extension code in the Node.js process without having to rely on the AMD loader especially for simplicity.


### Alternatives Considered

None

### Additional Information

_No response_


Discussion / hints:

Fix patch:
diff --git a/lib/utility/init.ts b/lib/utility/init.ts
index 5cf1ee1fdef9a..601299538788a 100644
--- a/lib/utility/init.ts
+++ b/lib/utility/init.ts
@@ -1,5 +1,7 @@
+import { pathToFileURL } from 'url';
+
 import { ParentPort } from '@electron/internal/utility/parent-port';
-const Module = require('module') as NodeJS.ModuleInternal;
+
 const v8Util = process._linkedBinding('electron_common_v8_util');
 
 const entryScript: string = v8Util.getHiddenValue(process, '_serviceStartupScript');
@@ -34,5 +36,14 @@ parentPort.on('removeListener', (name: string) => {
 });
 
 // Finally load entry script.
-process._firstFileName = Module._resolveFilename(entryScript, null, false);
-Module._load(entryScript, Module, true);
+const { loadESM } = __non_webpack_require__('internal/process/esm_loader');
+const mainEntry = pathToFileURL(entryScript);
+loadESM(async (esmLoader: any) => {
+  try {
+    await esmLoader.import(mainEntry.toString(), undefined, Object.create(null));
+  } catch (err) {
+    // @ts-ignore internalBinding is a secret internal global that we shouldn't
+    // really be using, so we ignore the type error instead of declaring it in types
+    internalBinding('errors').triggerUncaughtException(err);
+  }
+});
diff --git a/spec/api-utility-process-spec.ts b/spec/api-utility-process-spec.ts
index 278fbfba62ef2..f8b1280a31669 100644
--- a/spec/api-utility-process-spec.ts
+++ b/spec/api-utility-process-spec.ts
@@ -5,6 +5,7 @@ import { BrowserWindow, MessageChannelMain, utilityProcess } from 'electron/main
 import { ifit } from './lib/spec-helpers';
 import { closeWindow } from './lib/window-helpers';
 import { once } from 'node:events';
+import { pathToFileURL } from 'node:url';
 
 const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process');
 const isWindowsOnArm = process.platform === 'win32' && process.arch === 'arm64';
@@ -78,6 +79,12 @@ describe('utilityProcess module', () => {
       expect(code).to.equal(1);
     });
 
+    it('emits \'exit\' when there is uncaught exception in ESM', async () => {
+      const child = utilityProcess.fork(path.join(fixturesPath, 'exception.mjs'));
+      const [code] = await once(child, 'exit');
+      expect(code).to.equal(1);
+    });
+
     it('emits \'exit\' when process.exit is called', async () => {
       const exitCode = 2;
       const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
@@ -97,6 +104,23 @@ describe('utilityProcess module', () => {
     });
   });
 
+  describe('esm', () => {
+    it('is launches an mjs file', async () => {
+      const fixtureFile = path.join(fixturesPath, 'esm.mjs');
+      const child = utilityProcess.fork(fixtureFile, [], {
+        stdio: 'pipe'
+      });
+      await once(child, 'spawn');
+      expect(child.stdout).to.not.be.null();
+      let log = '';
+      child.stdout!.on('data', (chunk) => {
+        log += chunk.toString('utf8');
+      });
+      await once(child, 'exit');
+      expect(log).to.equal(pathToFileURL(fixtureFile) + '\n');
+    });
+  });
+
   describe('pid property', () => {
     it('is valid when child process launches successfully', async () => {
       const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
diff --git a/spec/fixtures/api/utility-process/esm.mjs b/spec/fixtures/api/utility-process/esm.mjs
new file mode 100644
index 0000000000000..748ec782dfc25
--- /dev/null
+++ b/spec/fixtures/api/utility-process/esm.mjs
@@ -0,0 +1,2 @@
+console.log(import.meta.url);
+process.exit(0);
diff --git a/spec/fixtures/api/utility-process/exception.mjs b/spec/fixtures/api/utility-process/exception.mjs
new file mode 100644
index 0000000000000..e6f79525dcba4
--- /dev/null
+++ b/spec/fixtures/api/utility-process/exception.mjs
@@ -0,0 +1,1 @@
+nonExistingFunc(); // eslint-disable-line no-undef


Test patch: